Skip to content

Export your grid data to an Excel (.xlsx) file, preserving cell types, styling, formulas, merged cells, and more.

Overview

The ExportFile plugin supports XLSX export via ExcelJS, which you must install as a peer dependency. XLSX export goes beyond CSV in several ways:

  • Cell types (numeric, date, time, checkbox, dropdown) are written as native Excel types with matching number formats.
  • Cell styling is read from the rendered DOM - font properties, background colors, alignment, and borders are all transferred to the workbook.
  • Column headers and nested headers, merged cells, frozen panes, hidden rows, and hidden columns are preserved.
  • HyperFormula and ColumnSummary destination cells can be exported as live Excel formulas.
  • Multiple Handsontable instances can be exported into separate sheets of one workbook.

Prerequisites

Install ExcelJS. The supported version range is ^4.4.0 (ExcelJS 4.x, version 4.4.0 or later).

Terminal window
npm install exceljs

Pass the ExcelJS constructor as engines: { xlsx: ExcelJS } in the exportFile plugin configuration:

import ExcelJS from 'exceljs';
const hot = new Handsontable(container, {
exportFile: { engines: { xlsx: ExcelJS } },
});

Example

The table below is a Q1 sales report that demonstrates the main XLSX export features: nested column headers, numeric and checkbox cell types, a column summary total, merged cells, a custom border, a frozen first column, and a cell comment on Alice’s row.

Click Export XLSX to download the file and open it in Microsoft Excel or any compatible spreadsheet application.

JavaScript
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
import ExcelJS from 'exceljs';
registerAllModules();
const container = document.querySelector('#example1');
const hot = new Handsontable(container, {
data: [
['Alice Martin', 'North', 142000, true, 'Exceeded Q1 target by 18%.'],
['Bob Chen', 'East', 98500, true, 'Strong pipeline for Q2.'],
['Carol Davies', 'South', 76200, false, 'Needs coaching on closing.'],
['David Kim', 'West', 115300, true, 'Cross-sell opportunity.'],
['Eva Rossi', 'North', 54800, false, 'Sick leave impacted March.'],
['TOTALS', '', null, '', ''],
],
nestedHeaders: [
[
{ label: 'Sales Representative', colspan: 2, headerClassName: 'htCenter' },
{ label: 'Results', colspan: 2, headerClassName: 'htCenter' },
{ label: 'Notes', colspan: 1, headerClassName: 'htLeft' },
],
['Name', 'Region', 'Revenue ($)', 'Hit Target?', 'Notes'],
],
columns: [
{ type: 'text' },
{ type: 'dropdown', source: ['North', 'South', 'East', 'West'] },
{
type: 'numeric',
locale: 'en-US',
numericFormat: { style: 'currency', currency: 'USD', minimumFractionDigits: 2 },
},
{ type: 'checkbox' },
{ type: 'text' },
],
columnSummary: [
{
sourceColumn: 2,
destinationRow: 5,
destinationColumn: 2,
type: 'sum',
forceNumeric: true,
},
],
mergeCells: [{ row: 5, col: 0, rowspan: 1, colspan: 2 }],
customBorders: [
{
row: 5,
col: 2,
top: { width: 2, color: '#333333' },
},
],
cell: [
{ row: 5, col: 0, readOnly: true },
{ row: 5, col: 2, readOnly: true },
],
fixedColumnsStart: 1,
rowHeaders: true,
colHeaders: false,
height: 'auto',
autoWrapRow: true,
autoWrapCol: true,
exportFile: { engines: { xlsx: ExcelJS } },
licenseKey: 'non-commercial-and-evaluation',
});
// Add a comment to Alice's notes cell.
hot.setCellMeta(0, 4, 'comment', { value: 'Top sales rep — review for promotion.' });
hot.render();
const exportPlugin = hot.getPlugin('exportFile');
const button = document.querySelector('#export-file');
button.addEventListener('click', async () => {
await exportPlugin.downloadFileAsync('xlsx', {
filename: 'Q1-Sales-Report',
columnHeaders: true,
rowHeaders: true,
exportFormulas: true,
});
});
TypeScript
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
import ExcelJS from 'exceljs';
registerAllModules();
const container = document.querySelector('#example1')!;
const hot = new Handsontable(container, {
data: [
['Alice Martin', 'North', 142000, true, 'Exceeded Q1 target by 18%.'],
['Bob Chen', 'East', 98500, true, 'Strong pipeline for Q2.'],
['Carol Davies', 'South', 76200, false, 'Needs coaching on closing.'],
['David Kim', 'West', 115300, true, 'Cross-sell opportunity.'],
['Eva Rossi', 'North', 54800, false, 'Sick leave impacted March.'],
['TOTALS', '', null, '', ''],
],
nestedHeaders: [
[
{ label: 'Sales Representative', colspan: 2, headerClassName: 'htCenter' },
{ label: 'Results', colspan: 2, headerClassName: 'htCenter' },
{ label: 'Notes', colspan: 1, headerClassName: 'htLeft' },
],
['Name', 'Region', 'Revenue ($)', 'Hit Target?', 'Notes'],
],
columns: [
{ type: 'text' },
{ type: 'dropdown', source: ['North', 'South', 'East', 'West'] },
{
type: 'numeric',
locale: 'en-US',
numericFormat: { style: 'currency', currency: 'USD', minimumFractionDigits: 2 },
},
{ type: 'checkbox' },
{ type: 'text' },
],
columnSummary: [
{
sourceColumn: 2,
destinationRow: 5,
destinationColumn: 2,
type: 'sum',
forceNumeric: true,
},
],
mergeCells: [
{ row: 5, col: 0, rowspan: 1, colspan: 2 },
],
customBorders: [
{
row: 5, col: 2,
top: { width: 2, color: '#333333' },
},
],
cell: [
{ row: 5, col: 0, readOnly: true },
{ row: 5, col: 2, readOnly: true },
],
fixedColumnsStart: 1,
rowHeaders: true,
colHeaders: false,
height: 'auto',
autoWrapRow: true,
autoWrapCol: true,
exportFile: { engines: { xlsx: ExcelJS } },
licenseKey: 'non-commercial-and-evaluation',
});
// Add a comment to Alice's notes cell.
hot.setCellMeta(0, 4, 'comment', { value: 'Top sales rep — review for promotion.' });
hot.render();
const exportPlugin = hot.getPlugin('exportFile');
const button = document.querySelector('#export-file')!;
button.addEventListener('click', async () => {
await exportPlugin.downloadFileAsync('xlsx', {
filename: 'Q1-Sales-Report',
columnHeaders: true,
rowHeaders: true,
exportFormulas: true,
});
});
HTML
<div class="example-controls-container">
<div class="controls">
<button id="export-file">Export XLSX</button>
</div>
</div>
<div id="example1"></div>

Available methods

The plugin exposes the following methods to export data.

Both methods take two parameters. The first, format, must be 'xlsx'. The second, options, is an optional object that configures the exported workbook.

Both methods are asynchronous. Use await or .then() to handle the result:

const exportPlugin = hot.getPlugin('exportFile');
// Download a file.
await exportPlugin.downloadFileAsync('xlsx', { filename: 'my-report' });
// Get a Blob (e.g. to upload to a server).
const blob = await exportPlugin.exportAsBlob('xlsx', { filename: 'my-report' });

Plugin configuration

Configure the plugin in Handsontable’s settings under the exportFile key.

OptionTypeDefaultDescription
enginesObject-A map of format keys to their engine constructors. Pass { xlsx: ExcelJS } to enable XLSX export via ExcelJS.

Export options

Pass these options as the second argument to downloadFileAsync('xlsx', options) or exportAsBlob('xlsx', options).

OptionType / DefaultDescription
filenameString, default 'Handsontable [YYYY]-[MM]-[DD]'File name without extension. Placeholders [YYYY], [MM], and [DD] are replaced with the current date.
columnHeadersBoolean, default falseInclude column headers in the exported file. Supports the NestedHeaders plugin.
rowHeadersBoolean, default falseInclude row headers as a frozen first column in the exported file.
exportHiddenColumnsBoolean | 'hide', default falseControls how hidden columns are handled. true exports them as normal visible columns. false omits them entirely. 'hide' exports them and marks them as hidden in Excel, so their data is preserved but not shown.
exportHiddenRowsBoolean | 'hide', default falseControls how hidden rows are handled. true exports them as normal visible rows. false omits them entirely. 'hide' exports them and marks them as hidden in Excel, so their data is preserved but not shown.
exportFormulasBoolean, default falseExport HyperFormula cells and ColumnSummary destination cells as live Excel formulas instead of their pre-calculated values.
sheetsArray, default []Multi-sheet configuration. Each entry is an object with an instance (a Handsontable object), a name (the sheet tab label), and any per-sheet options such as columnHeaders or rowHeaders. When provided, the top-level instance is ignored and each sheet is exported separately.
compressionBoolean | Number (1–9), default falseEnable DEFLATE compression. true uses level 6. A number 1–9 sets a specific level (1 = fastest, 9 = smallest).
conditionalFormattingArray, default []Array of conditional formatting descriptors. Each descriptor accepts optional rows and cols ranges (zero-based Handsontable indexes) and a rules array of ExcelJS conditional formatting rule objects.
rangeArray, default []Cell range to export: [startRow, startColumn, endRow, endColumn] (visual indexes). When omitted, the entire grid is exported.

Multi-sheet export

Use the sheets option to export multiple Handsontable instances into a single workbook. Each entry specifies the instance to read from and a name for the sheet tab.

Q1 Sales

Q2 Sales

JavaScript
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
import ExcelJS from 'exceljs';
registerAllModules();
const q1Data = [
['Alice Martin', 'North', 142000, true],
['Bob Chen', 'East', 98500, true],
['Carol Davies', 'South', 76200, false],
['David Kim', 'West', 115300, true],
['Eva Rossi', 'North', 54800, false],
];
const q2Data = [
['Alice Martin', 'North', 158000, true],
['Bob Chen', 'East', 112400, true],
['Carol Davies', 'South', 89100, true],
['David Kim', 'West', 97600, false],
['Eva Rossi', 'North', 63200, true],
];
const sharedConfig = {
columns: [
{ type: 'text' },
{ type: 'dropdown', source: ['North', 'South', 'East', 'West'] },
{
type: 'numeric',
locale: 'en-US',
numericFormat: { style: 'currency', currency: 'USD', minimumFractionDigits: 2 },
},
{ type: 'checkbox' },
],
colHeaders: ['Name', 'Region', 'Revenue ($)', 'Hit Target?'],
rowHeaders: true,
height: 'auto',
autoWrapRow: true,
autoWrapCol: true,
exportFile: { engines: { xlsx: ExcelJS } },
licenseKey: 'non-commercial-and-evaluation',
};
const hotQ1 = new Handsontable(document.querySelector('#example2-q1'), {
...sharedConfig,
data: q1Data,
});
const hotQ2 = new Handsontable(document.querySelector('#example2-q2'), {
...sharedConfig,
data: q2Data,
});
document.querySelector('#export-sheets').addEventListener('click', async () => {
const exportPlugin = hotQ1.getPlugin('exportFile');
await exportPlugin.downloadFileAsync('xlsx', {
filename: 'Annual-Sales-Report',
sheets: [
{ instance: hotQ1, name: 'Q1 Sales', columnHeaders: true, rowHeaders: true },
{ instance: hotQ2, name: 'Q2 Sales', columnHeaders: true, rowHeaders: true },
],
});
});
TypeScript
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
import ExcelJS from 'exceljs';
registerAllModules();
const q1Data = [
['Alice Martin', 'North', 142000, true ],
['Bob Chen', 'East', 98500, true ],
['Carol Davies', 'South', 76200, false],
['David Kim', 'West', 115300, true ],
['Eva Rossi', 'North', 54800, false],
];
const q2Data = [
['Alice Martin', 'North', 158000, true ],
['Bob Chen', 'East', 112400, true ],
['Carol Davies', 'South', 89100, true ],
['David Kim', 'West', 97600, false],
['Eva Rossi', 'North', 63200, true ],
];
const sharedConfig: Handsontable.GridSettings = {
columns: [
{ type: 'text' },
{ type: 'dropdown', source: ['North', 'South', 'East', 'West'] },
{
type: 'numeric',
locale: 'en-US',
numericFormat: { style: 'currency', currency: 'USD', minimumFractionDigits: 2 },
},
{ type: 'checkbox' },
],
colHeaders: ['Name', 'Region', 'Revenue ($)', 'Hit Target?'],
rowHeaders: true,
height: 'auto',
autoWrapRow: true,
autoWrapCol: true,
exportFile: { engines: { xlsx: ExcelJS } },
licenseKey: 'non-commercial-and-evaluation',
};
const hotQ1 = new Handsontable(document.querySelector('#example2-q1')!, {
...sharedConfig,
data: q1Data,
});
const hotQ2 = new Handsontable(document.querySelector('#example2-q2')!, {
...sharedConfig,
data: q2Data,
});
document.querySelector('#export-sheets')!.addEventListener('click', async () => {
const exportPlugin = hotQ1.getPlugin('exportFile');
await exportPlugin.downloadFileAsync('xlsx', {
filename: 'Annual-Sales-Report',
sheets: [
{ instance: hotQ1, name: 'Q1 Sales', columnHeaders: true, rowHeaders: true },
{ instance: hotQ2, name: 'Q2 Sales', columnHeaders: true, rowHeaders: true },
],
});
});
HTML
<div class="example-controls-container">
<div class="controls">
<button id="export-sheets">Export XLSX</button>
</div>
</div>
<p><strong>Q1 Sales</strong></p>
<div id="example2-q1"></div>
<p><strong>Q2 Sales</strong></p>
<div id="example2-q2"></div>

Context menu

When the context menu is enabled, Export to CSV and Export to Excel items are automatically added to the grid’s context menu. No extra configuration in exportFile is needed.

The Export to Excel item is only shown when an ExcelJS engine is configured via engines: { xlsx: ExcelJS }. The Export to CSV item is always available.

When you select a cell range before opening the context menu, the export covers only the selected range. When no selection is active, the entire grid is exported.

Right-click any cell to open the context menu.

JavaScript
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
import ExcelJS from 'exceljs';
registerAllModules();
new Handsontable(document.querySelector('#example3'), {
data: [
['Alice Martin', 'North', 142000, true],
['Bob Chen', 'East', 98500, true],
['Carol Davies', 'South', 76200, false],
['David Kim', 'West', 115300, true],
['Eva Rossi', 'North', 54800, false],
],
columns: [
{ type: 'text' },
{ type: 'dropdown', source: ['North', 'South', 'East', 'West'] },
{
type: 'numeric',
locale: 'en-US',
numericFormat: { style: 'currency', currency: 'USD', minimumFractionDigits: 2 },
},
{ type: 'checkbox' },
],
colHeaders: ['Name', 'Region', 'Revenue ($)', 'Hit Target?'],
rowHeaders: true,
height: 'auto',
autoWrapRow: true,
autoWrapCol: true,
contextMenu: true,
exportFile: { engines: { xlsx: ExcelJS } },
licenseKey: 'non-commercial-and-evaluation',
});
TypeScript
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
import ExcelJS from 'exceljs';
registerAllModules();
new Handsontable(document.querySelector('#example3')!, {
data: [
['Alice Martin', 'North', 142000, true ],
['Bob Chen', 'East', 98500, true ],
['Carol Davies', 'South', 76200, false],
['David Kim', 'West', 115300, true ],
['Eva Rossi', 'North', 54800, false],
],
columns: [
{ type: 'text' },
{ type: 'dropdown', source: ['North', 'South', 'East', 'West'] },
{
type: 'numeric',
locale: 'en-US',
numericFormat: { style: 'currency', currency: 'USD', minimumFractionDigits: 2 },
},
{ type: 'checkbox' },
],
colHeaders: ['Name', 'Region', 'Revenue ($)', 'Hit Target?'],
rowHeaders: true,
height: 'auto',
autoWrapRow: true,
autoWrapCol: true,
contextMenu: true,
exportFile: { engines: { xlsx: ExcelJS } },
licenseKey: 'non-commercial-and-evaluation',
});
HTML
<div class="example-controls-container">
<p>Right-click any cell to open the context menu.</p>
</div>
<div id="example3"></div>

Cell types and styling

The following Handsontable cell types are recognized and written to the .xlsx file with their native Excel equivalents.

Handsontable typeExcel behavior
numericNumber cell. The numericFormat option is translated to an Excel numFmt string using Intl.NumberFormat.
dateDate cell with an Excel date serial number. Reads ISO 8601 strings (YYYY-MM-DD).
timeTime cell with an Excel time serial number. Reads HH:mm, HH:mm:ss, and 12-hour (h:mm AM/PM) formats.
checkboxBoolean cell (TRUE / FALSE).
dropdown / autocompleteText cell. The validation list is not exported.
All othersText cell.

Cell styling is read from the rendered DOM at export time. The following properties are transferred to the workbook:

  • Font: bold, italic, underline, strikethrough, color, size, and family.
  • Fill: background color.
  • Alignment: horizontal (htLeft, htCenter, htRight, htJustify) and vertical (htTop, htMiddle, htBottom).
  • Borders: configurations set via the CustomBorders plugin. Border widths map to Excel styles: 1 px → thin, 2 px → medium, 3+ px → thick.

Read-only cells (readOnly: true) receive a light-gray fill and gray font color in the exported file by default. Applying CSS classes to a read-only cell overrides these defaults.

Plugins