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';
<HotTable
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 { useRef } from 'react';
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
import ExcelJS from 'exceljs';
registerAllModules();
const hotData = [
['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, '', ''],
];
const ExampleComponent = () => {
const hotRef = useRef(null);
const handleAfterInit = () => {
const hot = hotRef.current?.hotInstance;
hot?.setCellMeta(0, 4, 'comment', { value: 'Top sales rep — review for promotion.' });
hot?.render();
};
const exportFile = async () => {
const hot = hotRef.current?.hotInstance;
const exportPlugin = hot?.getPlugin('exportFile');
await exportPlugin?.downloadFileAsync('xlsx', {
filename: 'Q1-Sales-Report',
columnHeaders: true,
rowHeaders: true,
exportFormulas: true,
});
};
return (
<>
<div className="example-controls-container">
<div className="controls">
<button onClick={exportFile}>Export XLSX</button>
</div>
</div>
<HotTable
ref={hotRef}
data={hotData}
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 } }}
afterInit={handleAfterInit}
licenseKey="non-commercial-and-evaluation"
/>
</>
);
};
export default ExampleComponent;
TypeScript
import { useRef } from 'react';
import { HotTable, HotTableRef } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
import ExcelJS from 'exceljs';
registerAllModules();
const hotData = [
['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, '', ''],
];
const ExampleComponent = () => {
const hotRef = useRef<HotTableRef>(null);
const handleAfterInit = () => {
const hot = hotRef.current?.hotInstance;
hot?.setCellMeta(0, 4, 'comment', { value: 'Top sales rep — review for promotion.' });
hot?.render();
};
const exportFile = async () => {
const hot = hotRef.current?.hotInstance;
const exportPlugin = hot?.getPlugin('exportFile');
await exportPlugin?.downloadFileAsync('xlsx', {
filename: 'Q1-Sales-Report',
columnHeaders: true,
rowHeaders: true,
exportFormulas: true,
});
};
return (
<>
<div className="example-controls-container">
<div className="controls">
<button onClick={exportFile}>Export XLSX</button>
</div>
</div>
<HotTable
ref={hotRef}
data={hotData}
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 } }}
afterInit={handleAfterInit}
licenseKey="non-commercial-and-evaluation"
/>
</>
);
};
export default ExampleComponent;

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 = hotRef.current?.hotInstance?.getPlugin('exportFile');
await exportPlugin?.downloadFileAsync('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.

JavaScript
import { useRef } from 'react';
import { HotTable } from '@handsontable/react-wrapper';
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 columns = [
{ type: 'text' },
{ type: 'dropdown', source: ['North', 'South', 'East', 'West'] },
{
type: 'numeric',
locale: 'en-US',
numericFormat: { style: 'currency', currency: 'USD', minimumFractionDigits: 2 },
},
{ type: 'checkbox' },
];
const ExampleComponent = () => {
const hotQ1Ref = useRef(null);
const hotQ2Ref = useRef(null);
const exportSheets = async () => {
const hotQ1 = hotQ1Ref.current?.hotInstance;
const exportPlugin = hotQ1?.getPlugin('exportFile');
await exportPlugin?.downloadFileAsync('xlsx', {
filename: 'Annual-Sales-Report',
sheets: [
{ instance: hotQ1, name: 'Q1 Sales', columnHeaders: true, rowHeaders: true },
{ instance: hotQ2Ref.current?.hotInstance, name: 'Q2 Sales', columnHeaders: true, rowHeaders: true },
],
});
};
const sharedProps = {
columns,
colHeaders: ['Name', 'Region', 'Revenue ($)', 'Hit Target?'],
rowHeaders: true,
height: 'auto',
autoWrapRow: true,
autoWrapCol: true,
exportFile: { engines: { xlsx: ExcelJS } },
licenseKey: 'non-commercial-and-evaluation',
};
return (
<>
<div className="example-controls-container">
<div className="controls">
<button onClick={exportSheets}>Export XLSX</button>
</div>
</div>
<p>
<strong>Q1 Sales</strong>
</p>
<HotTable ref={hotQ1Ref} data={q1Data} {...sharedProps} />
<p>
<strong>Q2 Sales</strong>
</p>
<HotTable ref={hotQ2Ref} data={q2Data} {...sharedProps} />
</>
);
};
export default ExampleComponent;
TypeScript
import { useRef } from 'react';
import { HotTable, HotTableRef } from '@handsontable/react-wrapper';
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 columns = [
{ type: 'text' as const },
{ type: 'dropdown' as const, source: ['North', 'South', 'East', 'West'] },
{
type: 'numeric' as const,
locale: 'en-US',
numericFormat: { style: 'currency', currency: 'USD', minimumFractionDigits: 2 },
},
{ type: 'checkbox' as const },
];
const ExampleComponent = () => {
const hotQ1Ref = useRef<HotTableRef>(null);
const hotQ2Ref = useRef<HotTableRef>(null);
const exportSheets = async () => {
const hotQ1 = hotQ1Ref.current?.hotInstance;
const exportPlugin = hotQ1?.getPlugin('exportFile');
await exportPlugin?.downloadFileAsync('xlsx', {
filename: 'Annual-Sales-Report',
sheets: [
{ instance: hotQ1!, name: 'Q1 Sales', columnHeaders: true, rowHeaders: true },
{ instance: hotQ2Ref.current?.hotInstance!, name: 'Q2 Sales', columnHeaders: true, rowHeaders: true },
],
});
};
const sharedProps = {
columns,
colHeaders: ['Name', 'Region', 'Revenue ($)', 'Hit Target?'],
rowHeaders: true,
height: 'auto' as const,
autoWrapRow: true,
autoWrapCol: true,
exportFile: { engines: { xlsx: ExcelJS } },
licenseKey: 'non-commercial-and-evaluation' as const,
};
return (
<>
<div className="example-controls-container">
<div className="controls">
<button onClick={exportSheets}>Export XLSX</button>
</div>
</div>
<p><strong>Q1 Sales</strong></p>
<HotTable ref={hotQ1Ref} data={q1Data} {...sharedProps} />
<p><strong>Q2 Sales</strong></p>
<HotTable ref={hotQ2Ref} data={q2Data} {...sharedProps} />
</>
);
};
export default ExampleComponent;

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.

JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
import ExcelJS from 'exceljs';
registerAllModules();
const hotData = [
['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 ExampleComponent = () => (
<>
<div className="example-controls-container">
<p>Right-click any cell to open the context menu.</p>
</div>
<HotTable
data={hotData}
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"
/>
</>
);
export default ExampleComponent;
TypeScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
import ExcelJS from 'exceljs';
registerAllModules();
const hotData = [
['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 ExampleComponent = () => (
<>
<div className="example-controls-container">
<p>Right-click any cell to open the context menu.</p>
</div>
<HotTable
data={hotData}
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"
/>
</>
);
export default ExampleComponent;

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