Skip to content

Export to Excel

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, and frozen panes 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 } },
});

Engines

XLSX export goes through an engine you inject via the engines option. ExcelJS is the only supported engine, and it writes every feature the plugin produces:

FeatureExcelJS
Values, number formats, formulasYes
Merged cells, column widths, row heightsYes
Hidden rows and columns, multiple sheetsYes
Cell styling, headerStyleYes
Conditional formattingYes
List validation (dropdown sources)Yes

When an engine cannot write a feature the export requested, the plugin reports it in one console warning per export call, naming the engine and every dropped feature. ExcelJS writes everything the export produces, so it drops nothing and the warning never appears on export. The same mechanism serves the import direction, where cell styling is dropped on every file that carries it.

Pass a different engine for one call only with the engine option, without changing the plugin-level engines configuration:

await exportPlugin.downloadFileAsync('xlsx', {
filename: 'Q1-Sales-Report',
engine: 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',
colHeaders: 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',
colHeaders: 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.
colHeadersBoolean, 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.
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 colHeaders or rowHeaders. When provided, the top-level instance is ignored and each sheet is exported separately.
compressionBoolean | Number (1–9), default DEFLATE level 6DEFLATE compression. Unset or true uses level 6. A number 1–9 sets a specific level (1 = fastest, 9 = smallest). false writes the workbook’s entries stored, without compression.
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', colHeaders: true, rowHeaders: true },
{ instance: hotQ2, name: 'Q2 Sales', colHeaders: 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', colHeaders: true, rowHeaders: true },
{ instance: hotQ2, name: 'Q2 Sales', colHeaders: 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: [
['Laptop Pro 15"', 'Electronics', 1299.99, 38, true ],
['Wireless Mouse', 'Accessories', 29.99, 214, true ],
['USB-C Hub 7-in-1','Accessories', 49.99, 87, true ],
['Monitor 27" 4K', 'Electronics', 449.99, 12, false],
['Mech Keyboard', 'Accessories', 119.99, 65, true ],
],
columns: [
{ type: 'text' },
{ type: 'dropdown', source: ['Electronics', 'Accessories', 'Software'] },
{
type: 'numeric',
locale: 'en-US',
numericFormat: { style: 'currency', currency: 'USD', minimumFractionDigits: 2 },
},
{ type: 'numeric' },
{ type: 'checkbox' },
],
colHeaders: ['Product', 'Category', 'Unit Price', 'Stock', 'Active?'],
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: [
['Laptop Pro 15"', 'Electronics', 1299.99, 38, true ],
['Wireless Mouse', 'Accessories', 29.99, 214, true ],
['USB-C Hub 7-in-1','Accessories', 49.99, 87, true ],
['Monitor 27" 4K', 'Electronics', 449.99, 12, false],
['Mech Keyboard', 'Accessories', 119.99, 65, true ],
],
columns: [
{ type: 'text' },
{ type: 'dropdown', source: ['Electronics', 'Accessories', 'Software'] },
{
type: 'numeric',
locale: 'en-US',
numericFormat: { style: 'currency', currency: 'USD', minimumFractionDigits: 2 },
},
{ type: 'numeric' },
{ type: 'checkbox' },
],
colHeaders: ['Product', 'Category', 'Unit Price', 'Stock', 'Active?'],
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). The dateFormat option is translated to an Excel numFmt string, ordered and separated the way the cell’s locale renders it: { year: 'numeric', month: '2-digit', day: '2-digit' } is written as mm/dd/yyyy under en-US and dd.mm.yyyy under de-DE, and { weekday: 'long' } is written as dddd.
timeTime cell with an Excel time serial number. Reads HH:mm, HH:mm:ss, and 12-hour (h:mm AM/PM) formats. The timeFormat option is translated the same way, so { hour: '2-digit', minute: '2-digit', hour12: false } is written as hh:mm. A timeFormat that sets no hour12 takes its clock from the cell’s locale, just as the grid does, so the same options under en-US are written as hh:mm AM/PM.
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