# TabularJS: full documentation
> JavaScript library that converts spreadsheet files to JSON in Node.js and the browser. Generated from https://tabularjs.com; index at https://tabularjs.com/llms.txt.
## Rules for AI agents
Use these facts when writing code with TabularJS. They are the most common mistakes.
- Import: `import tabularjs from 'tabularjs'` (default export). With the CDN script (`https://cdn.jsdelivr.net/npm/tabularjs/dist/index.min.js`) the global is `tabularjs`.
- `tabularjs(file, options?)` returns a Promise: always `await` it. It only reads files; it does not write spreadsheets.
- Accepted `file`: a file path (Node.js), a `File` / `Blob` (browser), a `Uint8Array`, `ArrayBuffer` or Node `Buffer`, an HTML string, or a DOM element. A string starting with `<` is HTML; any other string is a file path.
- Bytes need no file name: the format is detected from the content. To force a format use `{ extension: 'csv' }`.
- The only options are `extension`, `delimiter`, `encoding`, `tableIndex`, `firstRowAsHeader`, `onload` and `onerror`. There is NO `name`, `format`, `type`, `sheet`, `worksheetIndex` or `header` option.
- Result: `{ worksheets: [...] }`. A sheet's name is `worksheet.worksheetName` (not `name`). Cell values are in `worksheet.data`, a 2D array of rows (`data[row][col]`).
- Formulas are returned as text in `data`, with the leading `=` (`"=SUM(A1:A3)"`), which is the place to read them for every format (only XLS and SYLK also copy them into `worksheet.cells["A1"].formula`). They are never evaluated, so a formula cell holds the formula, not its result.
- `worksheet.style["A1"]` is a number: an index into the top-level `result.style` array of CSS strings.
- `worksheet.mergeCells["A1"]` is `[colspan, rowspan]`. `worksheet.columns[i].title` is the column letter ("A", "B"...) for spreadsheet and CSV files, not the header text: the header row stays in `data[0]`. Only HTML tables move their first row into the titles, and DBF titles are the field names.
- CSV, TSV and TXT: every value is a string and the first row stays in `data`. Default delimiter: `,` for CSV, tab for TSV and TXT. Default encoding: UTF-8 (use `{ encoding: 'windows-1252' }` and similar for legacy files).
- HTML: every `
` becomes a worksheet (`Table1`, `Table2`...); `{ tableIndex: n }` returns only one. The first row becomes `columns[i].title` unless `{ firstRowAsHeader: false }`.
- Errors reject the promise (`Unsupported file type`, `Workbook is encrypted with a password`, `No table elements found in HTML`...): use try/catch.
- The result can be passed directly to Jspreadsheet: `jspreadsheet(element, result)`.
---
# Getting Started
> Install TabularJS and start parsing spreadsheet files to JSON. Step-by-step guide for Node.js, browser, React, Vue, and Angular with NPM and CDN options.
Source: https://tabularjs.com/docs/
TabularJS is a JavaScript library that converts spreadsheet files to JSON. It supports 16+ formats including Excel (.xls, .xlsx), OpenDocument (.ods), CSV, TSV, SYLK, DIF, dBase, Lotus 1-2-3, and HTML tables. It works in both Node.js and the browser, with only two small runtime dependencies (JSZip and an HTML table parser).
## Installation
### NPM
```bash
npm install tabularjs
```
### CDN
Load TabularJS directly from a CDN in your HTML:
```html
```
### Source Code
MIT licensed: [github.com/jspreadsheet/tabularjs](https://github.com/jspreadsheet/tabularjs)
## Quick Start
### Browser
Pass a `File` object from a file input or drag-and-drop event. TabularJS parses it on the client side and returns JSON.
```html
```
### Node.js
Pass a file path string. TabularJS reads the file from disk and returns the same JSON structure.
```js
import tabularjs from 'tabularjs';
const result = await tabularjs('path/to/file.xlsx');
console.log(result.worksheets[0].data);
```
### Raw bytes
Pass a `Uint8Array`, `ArrayBuffer` or Node `Buffer` when the file does not come from disk or a file input (a `fetch` response, a database, an upload stream). The format is detected from the content; use `extension` to force it.
```js
import tabularjs from 'tabularjs';
const response = await fetch('/reports/q3');
const bytes = new Uint8Array(await response.arrayBuffer());
const result = await tabularjs(bytes);
const csv = await tabularjs(bytes, { extension: 'csv' });
```
When a file name has no extension or an unknown one, TabularJS also falls back to detecting the format from the content.
### Command Line
Parse a spreadsheet file directly from the terminal:
```bash
node -e "import('tabularjs').then(m => m.default('file.xlsx').then(r => console.log(JSON.stringify(r, null, 2))))"
```
Or create a reusable script:
```js
import fs from 'fs';
import tabularjs from 'tabularjs';
const filePath = process.argv[2];
if (!filePath) {
console.log('Usage: node script.js ');
process.exit(1);
}
if (!fs.existsSync(filePath)) {
console.error(`Error: File not found: ${filePath}`);
process.exit(1);
}
const result = await tabularjs(filePath);
console.log(JSON.stringify(result, null, 2));
```
## Output Structure
Every file format produces the same JSON structure, ready to pass to Jspreadsheet:
```json
{
"worksheets": [
{
"worksheetName": "Q1 Report",
"data": [
["Name", "Revenue", "Growth"],
["North", 42000, "=B2*1.1"],
["South", 48500, "=Data!B3*1.1"]
],
"columns": [
{ "title": "A", "width": 120, "type": "text" },
{ "title": "B", "width": 90 },
{ "title": "C", "width": 90 }
],
"rows": { "0": { "height": 24 } },
"cells": { "A1": { "align": "center" } },
"style": { "A1": 0, "B1": 1 },
"mergeCells": { "A5": [3, 1] },
"comments": { "B2": "Includes returns" },
"minDimensions": [3, 3]
}
],
"style": [
"font-weight: bold; text-align: center",
"text-align: right"
],
"definedNames": { "Totals": "Sheet1!$B$2:$B$3" }
}
```
Formulas are kept as text in `data` (with the leading `=`), so references to other sheets, named ranges and array constants come through as Excel wrote them.
### Worksheet
| Property | Type | Description |
|----------|------|-------------|
| `worksheetName` | string | Sheet name (formats with a single table may omit it) |
| `data` | any[][] | 2D array of cell values: strings, numbers, booleans and formulas (`"=SUM(A1:A3)"`) |
| `columns` | object[] | One entry per column: `title`, plus `width`, `type`, `mask` and others when the format has them |
| `rows` | object | Row metadata keyed by row index, e.g. `{ "0": { "height": 24 } }` |
| `cells` | object | Per-cell properties keyed by address (`"A1"`): `format`, `align`, `wrap`... XLS and SYLK also copy each formula here as `formula` |
| `style` | object | Style of each cell, keyed by address: an index into the result's `style` list |
| `mergeCells` | object | Merged ranges keyed by the top-left address, value `[colspan, rowspan]` |
| `comments` | object | Cell comments keyed by address |
| `minDimensions` | [number, number] | `[columns, rows]` of the used range |
| `worksheetState` | string | `"hidden"` or `"veryHidden"`; omitted for visible sheets |
| `gridline` | boolean | `false` when the sheet hides its gridlines |
| `media` | object[] | Images, charts and shapes (XLSX, ODS) |
### Result
| Property | Type | Description |
|----------|------|-------------|
| `worksheets` | object[] | One worksheet per sheet in the file |
| `style` | string[] | Shared CSS styles referenced by each worksheet's `style` (XLS, XLSX, ODS) |
| `definedNames` | object | Named ranges, name to reference (XLSX, ODS) |
Rich formats like XLSX, XLS and ODS fill most of these properties. Text formats like CSV only emit `data` and `columns`. The full TypeScript definitions ship with the package (`tabularjs/dist/index.d.ts`).
## Framework Examples
Step-by-step guides for integrating TabularJS in your framework of choice.
- [React](/docs/react): Hooks + file input
- [Vue](/docs/vue): Composition API
- [Angular](/docs/angular): Standalone component
- [JavaScript](/docs/javascript): Vanilla / CDN
## Integration with Jspreadsheet
TabularJS output is directly compatible with [Jspreadsheet](https://jspreadsheet.com). Pass the result to `jspreadsheet()` to render uploaded files as live, editable spreadsheets.
### Vanilla JavaScript
```html
```
### React with Jspreadsheet
```jsx
import { useRef } from 'react';
import jspreadsheet from 'jspreadsheet-ce';
import tabularjs from 'tabularjs';
export default function App() {
const spreadsheet = useRef(null);
const inputRef = useRef(null);
const handleFile = async (e) => {
const file = e.target.files[0];
if (file) {
const result = await tabularjs(file);
jspreadsheet(spreadsheet.current, result);
}
};
return (
<>
>
);
}
```
## Supported Formats
| Format | Extension | Formulas | Styles | Merged Cells |
|--------|-----------|----------|--------|--------------|
| Excel 97-2003 | .xls | Yes | Yes | Yes |
| Excel 2007+ | .xlsx | Yes | Yes | Yes |
| OpenDocument | .ods | Yes | Yes | Yes |
| HTML Tables | .html, .htm | Partial | Yes | Yes |
| XML Spreadsheet 2003 | .xml | Yes | No | Yes |
| SYLK | .slk, .sylk | Yes | No | No |
| CSV | .csv | No | No | No |
| TSV | .tsv, .tab | No | No | No |
| Plain Text | .txt | No | No | No |
| DIF | .dif | No | No | No |
| dBase | .dbf | No | No | No |
| Lotus 1-2-3 | .wks, .wk1, .wk3, .wk4, .123 | No | No | No |
See the full [supported formats](/formats) gallery for per-format details and examples.
## TypeScript
TabularJS ships with TypeScript definitions. Import it normally and you get full type safety:
```ts
import tabularjs from 'tabularjs';
const result = await tabularjs('data.xlsx');
// result is fully typed
result.worksheets.forEach((sheet) => {
console.log(sheet.data);
});
```
## Testing
TabularJS includes 540 test cases covering all supported formats:
```bash
npm test
```
## Next Steps
- [API reference](/docs/api): every input, option and error
- [Supported Formats](/formats)
- [Changelog](/docs/changelog)
- [Jspreadsheet](https://jspreadsheet.com/docs)
- [GitHub](https://github.com/jspreadsheet/tabularjs)
- [How to read an Excel file in JavaScript](/docs/guides/read-excel-file-javascript)
- [How to read an Excel file in Node.js](/docs/guides/read-excel-file-nodejs)
- [How to parse a CSV file in JavaScript](/docs/guides/parse-csv-javascript)
- [How to convert Excel to JSON in JavaScript](/docs/guides/convert-excel-to-json)
---
# API reference
> Reference for tabularjs(file, options) - accepted inputs, format detection, every option, the result shape, errors, limits and upgrading from 1.x.
Source: https://tabularjs.com/docs/api/
TabularJS has a single function. It takes a file in any supported format and resolves with the same JSON structure for all of them.
```ts
tabularjs(file: FileInput, options?: ParseOptions): Promise
```
```js
import tabularjs from 'tabularjs';
const result = await tabularjs('report.xlsx');
```
:::tip[For AI agents]
This documentation is also available as plain text: [/llms.txt](/llms.txt) (index and rules for writing TabularJS code), [/llms-full.txt](/llms-full.txt) (everything in one file), and every page as Markdown by adding `.md` to its URL ([/docs/api.md](/docs/api.md)).
:::
## Inputs
| Input | Environment | Example |
|-------|-------------|---------|
| File path | Node.js | `tabularjs('./data/sales.xlsx')` |
| `File` / `Blob` | Browser | `tabularjs(input.files[0])` |
| `Uint8Array`, `ArrayBuffer`, Node `Buffer` | Both | `tabularjs(await response.arrayBuffer())` |
| HTML string | Both | `tabularjs('
...
')` |
| DOM element | Browser | `tabularjs(document.querySelector('table'))` |
A string that starts with `<` (leading whitespace allowed) is parsed as HTML; any other string is a file path.
### How the format is chosen
1. `options.extension`, when given. It is never overridden.
2. The extension of the file name (path or `File.name`), checked against the content:
- when the bytes clearly belong to another format (an `.xlsx` renamed `.xls`, an `.xls` saved as `.xlsx`), the real format is used;
- when the parser for the extension fails and the content looks like another format (a CSV, HTML or XML 2003 file saved as `.xls`, which is common), that format is tried. If it fails too, you get the first error.
3. The content of the file, when there is no name, no extension, or an extension TabularJS does not know. Zip containers (XLSX, ODS, Numbers), OLE2 (XLS), Lotus, dBase, SYLK, DIF, XML Spreadsheet, HTML, CSV and TSV are recognized from their bytes.
```js
// No file name: detected from the bytes
const result = await tabularjs(new Uint8Array(buffer));
// A .dat file that is really CSV
const csv = await tabularjs('export.dat', { extension: 'csv' });
```
## Options
| Option | Type | Default | Applies to | Description |
|--------|------|---------|------------|-------------|
| `extension` | `string` | (detected) | All | Force a format: `'xlsx'`, `'csv'`, `'.html'`... |
| `delimiter` | `string` | `','` for CSV, tab for TXT | CSV, TXT | Column separator. `.tsv` and `.tab` files always use tabs |
| `encoding` | `string` | `'utf-8'` (CSV, TSV, TXT, SYLK); detected (DIF); the file's codepage (DBF) | CSV, TSV, TXT, SYLK, DIF, DBF | Text encoding: any [WHATWG label](https://encoding.spec.whatwg.org/#names-and-labels) (`'latin1'`, `'windows-1251'`, `'shift_jis'`...). In Node.js, DOS codepages such as `'cp437'` and `'cp850'` also work through `iconv-lite` |
| `tableIndex` | `number` | (all tables) | HTML | Return only this table (0-based). Without it, every `
` becomes a worksheet |
| `firstRowAsHeader` | `boolean` | `true` | HTML | Use the first row as column titles instead of data |
| `onload` | `(result) => void` | | All | Called with the result when parsing succeeds |
| `onerror` | `(error) => void` | | All | Called with the error when parsing fails |
The returned promise is the main interface; `onload` and `onerror` are there for callback-style code. An unsupported format is reported before any parser runs, so it rejects the promise without calling `onerror`.
```js
// Semicolon-separated text file in a legacy Windows encoding
const result = await tabularjs(file, { extension: 'csv', delimiter: ';', encoding: 'windows-1252' });
// The second table of a page, keeping its first row as data
const table = await tabularjs(html, { tableIndex: 1, firstRowAsHeader: false });
```
## Result
`tabularjs` resolves with `{ worksheets: [...] }`, plus a shared `style` list and `definedNames` when the format has them. Each worksheet holds `data` (a 2D array, with formulas as text such as `"=SUM(A1:A3)"`) and, depending on the format, `worksheetName`, `columns`, `rows`, `cells`, `style`, `mergeCells`, `comments` and more.
See [Output structure](/docs#output-structure) for every property. The result can be passed straight to Jspreadsheet.
## Errors
Errors reject the promise (and call `onerror` when given). The most common ones:
| Message | Cause |
|---------|-------|
| `Unsupported file type: ...` | The format is not supported, or could not be recognized from a file without an extension |
| `Invalid file: must be a file path, File, Blob, Uint8Array, ArrayBuffer, or Buffer` | The input is not one of the [accepted inputs](#inputs) |
| `Workbook is encrypted with a password` | The `.xls` or `.xlsx` file is protected with a password to open |
| `Workbook is encrypted (XOR obfuscation); decryption is not supported` | The `.xls` file uses an encryption TabularJS cannot read (also `RC4 CryptoAPI`) |
| `No table elements found in HTML` | The HTML has no `
` |
| `Table index 3 out of range. Found 2 tables.` | `tableIndex` is larger than the number of tables |
| `Zip entry '...' exceeds the decompression limit of ... bytes (possible zip bomb)` | An XLSX, ODS or Numbers part inflates beyond the safety limit |
| `Encoding '...' requires iconv-lite package` | A DOS codepage was requested in an environment without `iconv-lite` |
Workbooks that are only write-protected ("read-only recommended" in Excel) are not an error: they open normally.
```js
try {
const result = await tabularjs(upload);
} catch (err) {
if (/encrypted/.test(err.message)) {
// Ask the user to save the file without a password
}
}
```
## Security and limits
TabularJS is designed to parse files uploaded by untrusted users:
- **Zip bombs**: every entry of an XLSX, ODS or Numbers file is capped at 256 MB once decompressed, counted on the actual inflated bytes rather than the size the file declares.
- **Crafted `.xls` files**: memory allocations are limited to the size of the container, and sector chains that loop are rejected.
- **Truncated files**: an `.xls` cut short by a failed download returns the part that could be read instead of failing.
- **No code execution**: formulas are returned as text and never evaluated; macros are ignored.
## Upgrading from 1.x
- **Node.js imports work.** 1.x could not be imported in Node.js because of a packaging bug. No code change is needed.
- **HTML returns every table.** `worksheets` now has one entry per `
`, named `Table1`, `Table2`... `worksheets[0]` is still the first table. Pass `tableIndex` to get only one.
- **Bytes need no file name.** Pass a `Uint8Array`, `ArrayBuffer` or `Buffer` directly. There is no `name` or `format` option: use `extension` to force a format.
- **Optional encodings in Node.js.** `iconv-lite` and `chardet` are now optional dependencies. They install by default; with `npm install --omit=optional`, DOS codepages (`cp437`, `cp850`...) are not available.
- **Undecodable `.xls` formulas** now show the value Excel saved for the cell instead of `=ERROR`.
See the [changelog](/docs/changelog) for the full list of changes.
---
# JavaScript
> Learn how to use TabularJS in vanilla JavaScript to parse Excel, CSV, and other spreadsheet files to JSON using a simple file input.
Source: https://tabularjs.com/docs/javascript/
Use TabularJS in vanilla JavaScript, no bundler required. Works in any modern browser or Node.js environment with a single function call.
## Installation
Install via npm:
```bash
npm install tabularjs
```
Or load directly from a CDN, no build step needed:
```html
```
## Basic Example
Listen for a file input change event and pass the selected file to `tabularjs`.
```js
import tabularjs from 'tabularjs';
const input = document.getElementById('fileInput');
input.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
const result = await tabularjs(file);
const [headers, ...rows] = result.worksheets[0].data;
console.log('Headers:', headers);
console.log('Rows:', rows);
});
```
## CDN Example
A self-contained HTML page: drop it into any project without a build tool.
```html
TabularJS CDN
```
## Node.js
Read a file from disk with `fs` and parse it server-side. A `Buffer` (or any `Uint8Array` / `ArrayBuffer`) can be passed directly; the format is detected from the content.
```js
import { readFile } from 'fs/promises';
import tabularjs from 'tabularjs';
const buffer = await readFile('./data.xlsx');
const result = await tabularjs(buffer);
const [headers, ...rows] = result.worksheets[0].data;
console.log('Headers:', headers);
console.log('Rows:', rows);
```
---
# React
> Learn how to use TabularJS in a React application to parse Excel, CSV, and other spreadsheet files to JSON using hooks and file input.
Source: https://tabularjs.com/docs/react/
Integrate TabularJS into a React application using hooks and a hidden file input to parse spreadsheet files directly in the browser.
## Installation
```bash
npm install tabularjs
```
## Basic Example
Use `useRef` to trigger the hidden file input and `useState` to store the parsed result.
```jsx
import { useRef, useState } from 'react';
import tabularjs from 'tabularjs';
export default function App() {
const [data, setData] = useState(null);
const inputRef = useRef(null);
const handleFile = async (e) => {
const file = e.target.files[0];
if (file) {
const result = await tabularjs(file);
setData(result);
}
};
return (
<>
{data && (
{JSON.stringify(data, null, 2)}
)}
>
);
}
```
## TypeScript
TabularJS ships with full TypeScript types. Use `TabularResult` for typed state.
```tsx
import { useRef, useState } from 'react';
import tabularjs, { TabularResult } from 'tabularjs';
export default function App() {
const [data, setData] = useState(null);
const inputRef = useRef(null);
const handleFile = async (e: React.ChangeEvent) => {
const file = e.target.files?.[0];
if (file) {
const result = await tabularjs(file);
setData(result);
}
};
return (
<>
{data && (
{JSON.stringify(data, null, 2)}
)}
>
);
}
```
---
# Vue
> Learn how to use TabularJS in a Vue 3 application to parse Excel, CSV, and other spreadsheet files to JSON using the Composition API.
Source: https://tabularjs.com/docs/vue/
Integrate TabularJS into a Vue 3 application using the Composition API to parse spreadsheet files directly in the browser.
## Installation
```bash
npm install tabularjs
```
## Composition API
Use `ref` for reactive state and a template ref to trigger the hidden file input.
```html
{{ JSON.stringify(data, null, 2) }}
```
## Options API
Prefer the classic Options API? TabularJS works just as well.
```html
{{ JSON.stringify(data, null, 2) }}
```
---
# Angular
> Learn how to use TabularJS in an Angular application to parse Excel, CSV, and other spreadsheet files to JSON using a standalone component.
Source: https://tabularjs.com/docs/angular/
Integrate TabularJS into an Angular application using a standalone component and a template reference to parse spreadsheet files in the browser.
## Installation
```bash
npm install tabularjs
```
## Standalone Component
Use a template reference variable (`#fileInput`) to trigger the hidden file input without needing `ViewChild`.
```ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import tabularjs from 'tabularjs';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule],
template: `
{{ data | json }}
`
})
export class AppComponent {
data: any = null;
async handleFile(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (file) {
this.data = await tabularjs(file);
}
}
}
```
## With ViewChild
Prefer programmatic access to the input element via `ViewChild`?
```ts
import { Component, ViewChild, ElementRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import tabularjs from 'tabularjs';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule],
template: `
{{ data | json }}
`
})
export class AppComponent {
@ViewChild('fileInput') fileInput!: ElementRef;
data: any = null;
openPicker() {
this.fileInput.nativeElement.click();
}
async handleFile(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (file) {
this.data = await tabularjs(file);
}
}
}
```
---
# Changelog
> TabularJS release notes - XLS formula decoding, buffer input, format detection by content, security hardening and fixes in each version.
Source: https://tabularjs.com/docs/changelog/
All notable changes to TabularJS. The full history is on [GitHub](https://github.com/jspreadsheet/tabularjs/blob/main/CHANGELOG.md).
## 2.2.0 (2026-10-29)
Lotus 1-2-3 WK3, WK4 and .123 files now return their data, files with the wrong extension open, and a long list of fixes found by testing real files from Excel, LibreOffice and openpyxl.
### Added
- **Lotus 1-2-3 WK3, WK4 and .123** (1-2-3 97 and Millennium): numbers, labels, formula results, error and N/A cells, notes, sheet names, column widths and hidden columns. Formulas return their saved results, as for WK1
- **Wrong extensions**: an `.xlsx` renamed `.xls`, an `.xls` saved as `.xlsx`, or a CSV, HTML or XML 2003 file saved as `.xls` is parsed as what it really is. `options.extension` is never overridden
- **Password-protected .xlsx** files report `Workbook is encrypted with a password`
- **SYLK**: the `encoding` option
### Fixed
- **XLSX**: shared formulas reached only the first cell of a filled range; every cell now gets its own formula. Inline strings (written by openpyxl and many generators) came back empty, `0` came back as `"0"`, error cells read `##VALUE!`, and shared strings kept `&` and phonetic readings
- **DIF**: the last row was duplicated, Excel files came back with as many columns as rows (52 x 52 instead of 52 x 17), and TRUE/FALSE, NA and ERROR came back as numbers
- **SYLK**: text with a semicolon (`;;`) was cut off
- **HTML**: only the first `` was read, `
` cells were moved in front of `
` cells, and entities were not decoded in Node.js
- **ODS**: comment text leaked into cell values, references to other sheets kept OpenDocument syntax (`[$Rates.A1]` now reads `Rates!A1`), and the lines of a multi-line cell were glued together
- **CSV**: a byte order mark stayed in the first cell when the file was passed as a path; UTF-16 files with a byte order mark are decoded correctly
- **Node.js**: `File` objects (from `FormData` in Next.js and other frameworks) failed for .xlsx and .ods
- 540 automated tests (up from 433)
## 2.1.1 (2026-10-28)
Fixes found by running the LibreOffice import test corpus against TabularJS.
### Fixed
- **dBase (.dbf)**: text fields are decoded with the codepage the file declares (Cyrillic, Central European, CJK...), or with `options.encoding` when given; before, they came out garbled
- **dBase (.dbf)**: a crafted file could declare millions of records it does not contain and exhaust memory; records are now limited to what the file holds
- **SYLK**: files with CR-only line endings (classic Mac Excel) parsed as empty
- **Lotus 1-2-3**: formula cells exposed raw token bytes as `cells[address].formula`; only the calculated value is returned now. String results of formulas no longer include stray header bytes
- **DIF / Numbers**: no more debug messages printed to the console while parsing
- 433 automated tests (up from 425)
## 2.1.0 (2026-10-28)
Legacy Excel files (`.xls`) now come through much more complete: formulas that point to other sheets, named ranges, array formulas and external workbooks are decoded, formatted text no longer corrupts the cells after it, and write-protected workbooks open normally.
### Highlights
- **Formulas across sheets in .xls**: `=Sheet2!A1`, `='My Sheet'!A1:B2`, ranges over several sheets (`=SUM(Jan:Dec!B2)`) and references to other workbooks (`='[Book2.xls]Prices'!A1`). References to deleted sheets become `#REF!`.
- **Named ranges in .xls**: `=SUM(Total)`, built-in names such as `Print_Area`, names from other workbooks and add-in / user-defined functions (`=MYUDF(A1)`).
- **Array formulas in .xls**: every cell of an array range gets its formula, and array constants such as `{1,2;"a",TRUE}` are decoded.
- **Encrypted .xls**: "read-only recommended" / write-protected workbooks open normally.
### Added
- XLS formulas: 3-D references (`tRef3d`, `tArea3d`, `tRefErr3d`, `tAreaErr3d`) resolved through the workbook's SUPBOOK and EXTERNSHEET records
- XLS formulas: defined names, built-in names, external names and add-in functions (`tName`, `tNameX`)
- XLS formulas: array formulas (ARRAY record) and array constants (`tArray`)
- XLS formulas: memory tokens (`tMemArea`, `tMemFunc`, ...) and the full set of error values (`#NULL!`, `#DIV/0!`, `#VALUE!`, `#REF!`, `#NAME?`, `#NUM!`, `#N/A`)
- Encrypted .xls: Office 97 RC4 workbooks protected with Excel's default password are decrypted; other passwords, RC4 CryptoAPI and XOR obfuscation raise a clear `Workbook is encrypted` error
- TypeScript definitions for byte input (`Uint8Array`, `ArrayBuffer`), `options.extension`, `worksheetState`, `gridline`, `media` and `definedNames`
- HTML strings and DOM elements as input: `tabularjs('
...
')`
- 425 automated tests (up from 374)
### Fixed
- **XLS text**: rich-text and phonetic (Far East) strings broke the shared string table, so every later cell read the wrong text, and strings past the first 8 KB were lost. On 400 real-world workbooks compared with xlrd, value mismatches dropped from about 40% of the files to zero
- **XLS sheets**: a chart embedded in a worksheet cut the sheet short, and a chart sheet shifted the data of every following sheet
- **XLS formula results**: boolean, error and empty-string results were read as numbers; error cells now read `#DIV/0!` and so on instead of `#ERROR7`
- **XLS formulas**: zero-argument functions (`NOW()`, `TODAY()`, `PI()`) swallowed the preceding operand
- **XLS formulas**: a formula that cannot be decoded exactly now shows the value Excel saved for the cell instead of `=ERROR` or a wrong formula
- **XLS**: inline strings written by older tools (LABEL records) were ignored
- **HTML**: every `
` on a page now becomes a worksheet (only the first one was returned before); use `tableIndex` to pick one
- **CSV**: the `encoding` option was ignored (files were always read as UTF-8)
- **TXT**: a `delimiter` passed for `.txt` files was overridden by tab
- **Truncated .xls files**: files cut short by a download now return the readable part instead of failing with `Sector out of bounds`
## 2.0.0 (2026-10-09)
The package now imports correctly in Node.js, accepts raw bytes, and detects the format from the file content. It also adds protection against malicious files and ships a much smaller browser bundle.
### Raw bytes and the `extension` option
```js
import tabularjs from 'tabularjs';
// From fetch, a database blob, an upload stream...
const bytes = new Uint8Array(await (await fetch('/report')).arrayBuffer());
// The format is detected from the content...
const result = await tabularjs(bytes);
// ...or forced explicitly
const csv = await tabularjs(bytes, { extension: 'csv' });
```
### Added
- `tabularjs(uint8Array | arrayBuffer | buffer)` input, plus an `options.extension` override
- Content-based format detection (magic bytes) when the extension is missing or unrecognized: xlsx, ods, numbers, xls (OLE2), Lotus, dBase, SYLK, DIF, XML, HTML, CSV and TSV
- Apple Numbers (`.numbers`) parser (experimental)
- Zip-bomb protection on every xlsx, ods and numbers entry, and allocation limits on OLE2 containers (crafted `.xls` protection)
- 374 automated tests (up from 312)
### Fixed
- **Packaging**: the npm package could not be imported in Node.js at all. An `exports` map now gives Node the ESM source, bundlers a real ES module (`dist/index.mjs`), and `
```
## Parse a CSV file in Node.js
Given `people.csv`:
```
name,email,age,city
Ada Lovelace,ada@example.com,36,London
"Hopper, Grace",grace@example.com,85,"New York"
Alan Turing,ALAN@example.com,,Wilmslow
```
Pass the path:
```js
import tabularjs from 'tabularjs';
const result = await tabularjs('./data/people.csv');
console.log(result.worksheets[0].data);
```
```js
[
[ 'name', 'email', 'age', 'city' ],
[ 'Ada Lovelace', 'ada@example.com', '36', 'London' ],
[ 'Hopper, Grace', 'grace@example.com', '85', 'New York' ],
[ 'Alan Turing', 'ALAN@example.com', '', 'Wilmslow' ]
]
```
What to expect from CSV input:
- A CSV file has one table, so `result.worksheets` has a single entry. It has no `worksheetName`.
- `data` is an array of rows; each row is an array of strings. **Every value is a string**, including `'36'`. CSV has no types, so TabularJS does not guess them.
- The header row stays in `data[0]`. It is not removed or used as keys.
- Quoted fields are unquoted: `"Hopper, Grace"` becomes `Hopper, Grace`, `""` becomes `"`, and a quoted value can span several lines.
- Missing values are `''`. A trailing newline at the end of the file does not add an empty row.
The worksheet also has `columns`, with `title` set to the column letter (`'A'`, `'B'`...). Text formats do not carry styles or formulas.
## Parse a CSV file in the browser
In the browser, pass the `File` from an input or a drop event. The call and the result are identical to Node.js.
```html
```
For a CSV on a server, fetch it and pass the bytes with `extension: 'csv'`, so it is not mistaken for another text format:
```js
const response = await fetch('/exports/people.csv');
const bytes = new Uint8Array(await response.arrayBuffer());
const result = await tabularjs(bytes, { extension: 'csv' });
```
To parse CSV text you already have in a string, encode it first. A string passed directly is treated as a file path (or as HTML when it starts with `<`):
```js
const text = 'sku,qty\nA-1,4\nB-2,10\n';
const bytes = new TextEncoder().encode(text);
const result = await tabularjs(bytes, { extension: 'csv' });
console.log(result.worksheets[0].data);
// [ [ 'sku', 'qty' ], [ 'A-1', '4' ], [ 'B-2', '10' ] ]
```
## Turn rows into objects using the header row
Most code wants `{ name: 'Ada Lovelace', email: '...' }` instead of positional arrays. Take the first row as keys and map the rest:
```js
import tabularjs from 'tabularjs';
function csvToObjects(rows) {
const [header = [], ...body] = rows;
// TabularJS 2.2+ already strips the byte order mark; this keeps older versions safe
const keys = header.map((h, i) => h.replace(/^/, '').trim() || `column${i + 1}`);
return body
.filter((row) => row.some((v) => v.trim() !== ''))
.map((row) => Object.fromEntries(keys.map((key, i) => [key, row[i] ?? ''])));
}
const { worksheets } = await tabularjs('./data/people.csv');
const people = csvToObjects(worksheets[0].data);
console.log(people);
```
```js
[
{ name: 'Ada Lovelace', email: 'ada@example.com', age: '36', city: 'London' },
{ name: 'Hopper, Grace', email: 'grace@example.com', age: '85', city: 'New York' },
{ name: 'Alan Turing', email: 'ALAN@example.com', age: '', city: 'Wilmslow' }
]
```
The helper handles three things that bite in practice:
- **Byte order mark.** Excel's "CSV UTF-8" export starts the file with an invisible ``. TabularJS 2.2 and later remove it for every kind of input. Older versions left it glued to the first header (`'name'`) when you passed a Node.js file path, and then `row.name` is `undefined` even though the console shows `name`. Stripping it again costs nothing.
- **Blank headers.** A column without a header gets `column3`, `column4`... instead of an empty-string key.
- **Blank lines.** A blank line in the middle of the file is returned as a row of `''` values. The filter drops it.
If two columns share the same header, the later one wins in `Object.fromEntries`. Rename duplicates before mapping if your files can have them.
### Convert the strings to numbers and dates
Since all values are strings, convert the fields you need explicitly. Doing it per field is clearer than guessing types for the whole file, and it avoids classic accidents such as zip code `'02139'` becoming `2139`.
```js
const people = csvToObjects(worksheets[0].data).map((p) => ({
name: p.name,
email: p.email.toLowerCase(),
age: p.age === '' ? null : Number(p.age),
city: p.city,
}));
```
```js
[
{ name: 'Ada Lovelace', email: 'ada@example.com', age: 36, city: 'London' },
{ name: 'Hopper, Grace', email: 'grace@example.com', age: 85, city: 'New York' },
{ name: 'Alan Turing', email: 'alan@example.com', age: null, city: 'Wilmslow' }
]
```
Watch for `Number('')`, which is `0`, not `NaN`. Check for empty strings first, as above. For dates, prefer ISO strings (`2024-03-15`) in your exports; `new Date('03/04/2024')` is ambiguous between March and April.
## Semicolon-separated files and the `delimiter` option
The default delimiter for `.csv` is a comma. In many European locales, Excel saves "CSV" with semicolons because the comma is the decimal separator. Parsed with the default, such a file comes out wrong:
```
name;amount;city
José;1.234,50;München
Zoë;99,00;Zürich
```
```js
const wrong = await tabularjs('./data/euro.csv');
console.log(wrong.worksheets[0].data[1]);
// [ 'Jos�;1.234', '50;M�nchen' ]
```
Two problems: the columns are split at the decimal commas, and the accented letters are broken. The second one is an [encoding issue](#legacy-encodings-and-the-encoding-option). Fix both with options:
```js
const result = await tabularjs('./data/euro.csv', {
delimiter: ';',
encoding: 'windows-1252',
});
console.log(result.worksheets[0].data);
// [
// [ 'name', 'amount', 'city' ],
// [ 'José', '1.234,50', 'München' ],
// [ 'Zoë', '99,00', 'Zürich' ]
// ]
// "1.234,50" -> 1234.5
const parseEuro = (s) => Number(s.replace(/\./g, '').replace(',', '.'));
console.log(result.worksheets[0].data.slice(1).map((row) => parseEuro(row[1])));
// [ 1234.5, 99 ]
```
TabularJS does not guess the delimiter. If users upload files from different locales, count the candidates in the first line and pick the most frequent:
```js
import { readFile } from 'node:fs/promises';
import tabularjs from 'tabularjs';
function guessDelimiter(sample) {
const firstLine = sample.split(/\r?\n/)[0];
const candidates = [',', ';', '\t', '|'];
const counts = candidates.map((d) => firstLine.split(d).length - 1);
const best = counts.indexOf(Math.max(...counts));
return counts[best] > 0 ? candidates[best] : ',';
}
const bytes = await readFile('./data/euro.csv');
const sample = new TextDecoder('windows-1252').decode(bytes.subarray(0, 4096));
const delimiter = guessDelimiter(sample); // ';'
const result = await tabularjs(bytes, { extension: 'csv', delimiter, encoding: 'windows-1252' });
```
In the browser, read the sample with `await file.slice(0, 4096).text()` and pass `file` itself to `tabularjs`. Counting on the header line is a heuristic: it is reliable for typical exports, but let users override it when it guesses wrong.
## Legacy encodings and the `encoding` option
TabularJS decodes CSV, TSV and TXT as UTF-8 by default. Files exported by older Windows software, or by Excel's plain "CSV (Comma delimited)" option on Windows, are usually in Windows-1252. Read as UTF-8, every accented character becomes `�`, as in the example above.
Set `encoding` to any label that `TextDecoder` understands:
| Source | `encoding` |
|--------|------------|
| Excel / Windows, Western Europe and Americas | `'windows-1252'` (or `'latin1'`, same decoder) |
| Windows, Central Europe | `'windows-1250'` |
| Windows, Cyrillic | `'windows-1251'` |
| Japanese | `'shift_jis'` |
| Chinese (Simplified) | `'gbk'` |
In Node.js, old DOS codepages such as `'cp437'` and `'cp850'` also work, through the optional `iconv-lite` dependency. See the [`encoding` option](/docs/api#options).
There is no reliable way to detect an 8-bit encoding from the bytes alone. A practical approach for uploads: parse as UTF-8 first, and if any value contains `�` (the `�` replacement character), parse again with `windows-1252` or let the user pick the encoding.
## TSV and TXT files
Tab-separated files use the same parser with a tab delimiter. `.tsv` and `.tab` files always use tabs. `.txt` files default to tabs and accept a `delimiter` for anything else:
```js
const tsv = await tabularjs('./data/scores.tsv'); // tab by default
const txt = await tabularjs('./data/pipe.txt', { delimiter: '|' });
console.log(tsv.worksheets[0].data);
// [ [ 'id', 'name', 'score' ], [ '1', 'Ada', '9.5' ], [ '2', 'Grace', '8' ] ]
console.log(txt.worksheets[0].data);
// [ [ 'id', 'name' ], [ '1', 'Ada' ], [ '2', 'Grace' ] ]
```
A `.txt` file that is actually comma-separated needs `{ delimiter: ',' }`, or `{ extension: 'csv' }`. The `csvToObjects` helper above works unchanged for all of them.
## Common pitfalls
**Columns shifted by one.** Almost always a delimiter mismatch: a semicolon file read with commas, or a comma inside an unquoted value written by a broken exporter. Check `data[0].length` against the number of columns you expect and fail early.
**Garbled accents.** The file is not UTF-8. Set `encoding`.
**Header key not found.** A BOM or trailing spaces in the header. Normalize keys with `.replace(/^/, '').trim()`.
**Leading zeros and long numbers.** IDs like `'00451'` or 16-digit card numbers are safe as strings. Converting every numeric-looking value to a number would corrupt them.
**Formula injection on export.** If you write user-supplied CSV values back out to a file someone opens in Excel, values that start with `=`, `+`, `-` or `@` can run as formulas. TabularJS never evaluates anything when reading, but escape such values when you generate CSV.
**Huge files.** TabularJS parses the whole file in memory. That is fast for typical files (200,000 rows parse in a fraction of a second in Node.js), but for multi-gigabyte logs, a streaming parser such as Papa Parse or `csv-parse` that emits rows one at a time is the better tool.
**Mixed uploads.** If the same import accepts CSV and Excel, one `tabularjs(file)` call reads both. Remember that XLSX values are typed (numbers, booleans) while CSV values are strings, and normalize before comparing.
## Related
- [API reference](/docs/api): the `delimiter`, `encoding` and `extension` options
- Formats: [CSV](/formats/csv), [TSV](/formats/tsv), [plain text](/formats/txt)
- [Read an Excel file in JavaScript](/docs/guides/read-excel-file-javascript)
- [Convert Excel to JSON](/docs/guides/convert-excel-to-json)
- Examples: [browser file input](/examples/browser-file-input), [URL response](/examples/csv-streaming), [Express upload](/examples/express-upload)
---
# How to read an Excel file in JavaScript
> Read .xlsx and .xls files in the browser with JavaScript. File input, drag and drop and fetch examples, the data you get back, dates and common pitfalls.
Source: https://tabularjs.com/docs/guides/read-excel-file-javascript/
Browsers cannot open Excel files on their own. An `.xlsx` file is a zip archive of XML documents, and an `.xls` file is a binary format from Excel 97-2003, so you need a parser that turns the bytes into JavaScript values. This guide shows how to do that in the browser with [TabularJS](/docs), from the three places a file usually comes from: a file input, a drag and drop zone, and a URL.
Everything runs on the client. The file is never uploaded, which makes this a good fit for previews, import screens and offline tools.
If you are reading files on a server, see [Read an Excel file in Node.js](/docs/guides/read-excel-file-nodejs) instead.
## Install
With a bundler (Vite, webpack, Next.js...):
```bash
npm install tabularjs
```
```js
import tabularjs from 'tabularjs';
```
Without a build step, load the script from a CDN. It defines a global `tabularjs` function:
```html
```
Both give you the same function: `tabularjs(file, options)`, which returns a Promise.
## Read a file from an ``
This is the most common case. The user picks a file, the `change` event gives you a `File` object, and you pass it straight to `tabularjs`.
```html
```
The `accept` attribute only filters the file picker. Users can still choose "All files", so keep the `try/catch`.
You do not need `FileReader`. TabularJS reads the `File` itself and picks the parser from the file name (`.xlsx` or `.xls`), falling back to the content when the name has no known extension.
## What you get back
Take a workbook with two sheets, `Orders` and `Customers`. The `Orders` sheet looks like this in Excel:
| Order | Customer | Date | Qty | Price | Total | Paid |
|-------|----------|------|-----|-------|-------|------|
| 1001 | Acme Corp | 2024-03-15 | 3 | 19.99 | `=D2*E2` | TRUE |
| 1002 | Globex | 2024-03-18 | 8 | 5.5 | `=D3*E3` | FALSE |
`tabularjs(file)` resolves with one entry per sheet in `worksheets`:
```js
{
worksheets: [
{
worksheetName: 'Orders',
data: [
['Order', 'Customer', 'Date', 'Qty', 'Price', 'Total', 'Paid'],
[1001, 'Acme Corp', 45366, 3, 19.99, '=D2*E2', true],
[1002, 'Globex', 45369, 8, 5.5, '=D3*E3', false]
],
columns: [{ title: 'A', width: 100, type: 'text' }, /* ... */],
cells: { C2: { format: 'yyyy-mm-dd' }, C3: { format: 'yyyy-mm-dd' } },
// rows, style, mergeCells, comments, minDimensions...
},
{
worksheetName: 'Customers',
data: [['Name', 'Country'], ['Acme Corp', 'US'], ['Globex', 'DE']],
// ...
}
],
style: ['font-family: Calibri; font-size: 11pt']
}
```
The parts you will use most:
- `worksheet.data` is a 2D array: `data[row][col]`, both zero-based. `data[0]` is the first row of the sheet, which is usually the header.
- Numbers come back as numbers and TRUE/FALSE as booleans. Text stays a string.
- The grid is rectangular. Empty cells are `''`, including empty rows in the middle of the sheet.
- Formulas are returned as text with the leading `=` (`'=D2*E2'`). They are not calculated. See [Formulas](#formulas-are-not-calculated) below.
- `worksheet.worksheetName` is the tab name. There is no `name` property.
The full list of properties (styles, merged cells, comments, column widths) is in [Output structure](/docs#output-structure).
## Show the data in an HTML table
Once you have `data`, rendering it is plain DOM work. Use `textContent` rather than `innerHTML` so a cell that contains `
```
To accept several files at once, loop over `e.dataTransfer.files` and `await tabularjs(f)` for each one. A complete page is in the [drag and drop example](/examples/drag-and-drop).
## Read an Excel file from a URL with fetch
When the file lives on your server or a storage bucket, download it with `fetch`, read the body as an `ArrayBuffer` and pass that. Raw bytes have no file name, so TabularJS detects the format from the content: XLSX and XLS are both recognized.
```js
async function loadWorkbook(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Download failed: ${response.status} ${response.statusText}`);
}
const bytes = new Uint8Array(await response.arrayBuffer());
return tabularjs(bytes);
}
const result = await loadWorkbook('/reports/q3-sales.xlsx');
console.log(result.worksheets[0].data);
```
Check `response.ok`. A missing file often returns a 404 HTML page, and TabularJS will try to parse that page as HTML and reject with `No table elements found in HTML`, which is a confusing message if you were expecting a spreadsheet.
The same code works in Node.js 18+, Deno, Bun and edge runtimes, since they all have `fetch`. See the [URL response example](/examples/csv-streaming).
## Dates come back as numbers
Excel does not store dates as dates. It stores a number of days since 1899-12-30, and the cell's number format makes Excel display it as a date. TabularJS returns that stored number, and puts the number format in `worksheet.cells`:
```js
const sheet = result.worksheets[0];
sheet.data[1][2]; // 45366
sheet.cells.C2.format; // 'yyyy-mm-dd'
```
To turn the serial number into a JavaScript `Date`:
```js
function excelDateToJS(serial) {
return new Date(Math.round((serial - 25569) * 86400 * 1000));
}
excelDateToJS(45366).toISOString(); // '2024-03-15T00:00:00.000Z'
excelDateToJS(45371.60416666666).toISOString(); // '2024-03-20T14:30:00.000Z'
```
The fractional part is the time of day. The result is in UTC because Excel dates have no time zone; format it with `toISOString().slice(0, 10)` or `Intl.DateTimeFormat` with `timeZone: 'UTC'` to avoid it shifting by a day in your user's time zone. The [Excel to JSON guide](/docs/guides/convert-excel-to-json#dates) shows how to convert every date column automatically using the cell formats.
## Formulas are not calculated
A cell with `=D2*E2` comes back as the string `'=D2*E2'`, not the value Excel displayed. TabularJS never evaluates formulas, which keeps it safe for untrusted files and preserves the formula itself. If you need computed values, you have two options: load the result into a spreadsheet component that has a formula engine (such as Jspreadsheet), or ask users to paste values (Paste Special > Values) before exporting. More in [Formulas](/docs/guides/convert-excel-to-json#formulas).
## Common pitfalls
**Password-protected workbooks.** An `.xls` file encrypted with a password to open rejects with `Workbook is encrypted with a password`. Show the user a clear message asking for an unprotected copy. Files that are only "read-only recommended" open normally.
**Wrong file type.** A PDF or image renamed or chosen by mistake rejects with `Unsupported file type: ...`. Catch errors around every call; see the [list of errors](/docs/api#errors).
**Large files freeze the page.** Parsing happens on the main thread. For workbooks of tens of megabytes, show a loading state before calling `tabularjs` and consider running it in a Web Worker so the UI stays responsive.
**Opening the page from `file://`.** `fetch('/data.xlsx')` does not work from a page opened by double-clicking the HTML file. Serve the folder with any local web server (`npx serve`, for example). The file input and drag and drop work either way.
**Cross-origin downloads.** Fetching a file from another domain needs CORS headers on that server. If you control the bucket, allow your origin; otherwise download it through your own backend.
**`.xlsm` and CSV.** Macro-enabled `.xlsm` workbooks use the same zip layout as `.xlsx`: they are detected from the content and the macros are ignored. If your users also upload CSV files, the same call handles them, but every value is a string: see [Parse CSV in JavaScript](/docs/guides/parse-csv-javascript).
## When something else fits better
TabularJS only reads files. If you need to generate an Excel file in the browser, use a library that writes XLSX, such as ExcelJS or SheetJS. If your data is already in a Google Sheet, its API or a published CSV link is simpler than downloading an `.xlsx`.
## Related
- [API reference](/docs/api): inputs, options and errors
- [Excel 2007+ (.xlsx)](/formats/xlsx) and [Excel 97-2003 (.xls)](/formats/xls) format details
- [Convert Excel to JSON](/docs/guides/convert-excel-to-json): rows to objects, all sheets, dates
- [JavaScript](/docs/javascript), [React](/docs/react), [Vue](/docs/vue) and [Angular](/docs/angular) integration guides
- Examples: [browser file input](/examples/browser-file-input), [drag and drop](/examples/drag-and-drop)
---
# How to read an Excel file in Node.js
> Read .xlsx and .xls files in Node.js from a path or a Buffer, handle uploads with Express and multer, and batch-convert a folder. Runnable code and output.
Source: https://tabularjs.com/docs/guides/read-excel-file-nodejs/
This guide covers the server side of reading Excel files: opening a workbook from disk, parsing one that is already in memory as a `Buffer`, accepting uploads in an Express API, and processing a whole folder. It uses [TabularJS](/docs), which reads `.xlsx` and `.xls` (plus ODS, CSV and a dozen other formats) and returns plain arrays.
Every snippet below is an ES module. Save it as `.mjs`, or add `"type": "module"` to your `package.json`, and run it with Node.js 18 or later.
## Install
```bash
npm install tabularjs
```
```js
import tabularjs from 'tabularjs';
```
TabularJS has one function, `tabularjs(file, options)`, and it returns a Promise. In Node.js, `file` can be a path or bytes (`Buffer`, `Uint8Array`, `ArrayBuffer`).
## Read a workbook from a file path
Pass the path. TabularJS reads the file and chooses the parser from the extension.
```js
import tabularjs from 'tabularjs';
const result = await tabularjs('./data/orders.xlsx');
for (const sheet of result.worksheets) {
console.log(sheet.worksheetName, sheet.data.length, 'rows');
}
const orders = result.worksheets[0];
console.log(orders.data);
```
Output:
```
Orders 4 rows
Customers 3 rows
[
[ 'Order', 'Customer', 'Date', 'Qty', 'Price', 'Total', 'Paid' ],
[ 1001, 'Acme Corp', 45366, 3, 19.99, '=D2*E2', true ],
[ 1002, 'Globex', 45369, 8, 5.5, '=D3*E3', false ],
[ 1003, 'Initech', 45371.60416666666, 12, 2.25, '=D4*E4', true ]
]
```
A few things to notice in that output:
- `result.worksheets` has one entry per sheet, in workbook order. The tab name is `worksheetName`.
- `data` is an array of rows, and each row is an array of cell values: `data[row][col]`, zero-based. The header row is `data[0]`; it is not removed.
- Numbers are numbers and TRUE/FALSE are booleans. The `Date` column holds numbers too (see [Dates](#dates-are-serial-numbers)).
- `Total` contains the formula text, not its result. TabularJS does not evaluate formulas.
- Empty cells are `''`, and every row has the same length.
The same code reads an `.xls` file: change the path, nothing else. Relative paths are resolved from the current working directory, not from the script's folder. If you run scripts from different places, build the path from `import.meta.dirname` (Node 20.11+) or `process.cwd()` explicitly.
Each worksheet also has `columns`, `cells`, `style`, `mergeCells`, `comments` and more. They are described in [Output structure](/docs#output-structure). To turn rows into objects keyed by the header, see [Convert Excel to JSON](/docs/guides/convert-excel-to-json).
## Read from a Buffer
Files do not always come from disk. Downloads from S3, attachments from an email parser, rows from a `bytea` column: they all give you a `Buffer`. Pass it directly. Bytes have no file name, so the format is detected from the content. XLSX (a zip container) and XLS (an OLE2 container) are both recognized.
```js
import { readFile } from 'node:fs/promises';
import tabularjs from 'tabularjs';
const buffer = await readFile('./data/orders.xls');
const result = await tabularjs(buffer); // format detected from the bytes
console.log(result.worksheets.map((s) => s.worksheetName));
// [ 'Orders', 'Customers' ]
```
For a remote file, `fetch` returns an `ArrayBuffer`, which works the same way:
```js
const response = await fetch('https://example.com/report.xlsx');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await tabularjs(await response.arrayBuffer());
```
If you know the bytes are CSV and need options such as a delimiter, pass `extension: 'csv'` so detection is skipped. See the [API reference](/docs/api#how-the-format-is-chosen) for the detection order.
## Handle Excel uploads in Express with multer
For an upload endpoint, keep the file in memory with multer's `memoryStorage()` and pass `req.file.buffer` to TabularJS. Nothing is written to disk.
```bash
npm install express multer tabularjs
```
```js
import express from 'express';
import multer from 'multer';
import path from 'node:path';
import tabularjs from 'tabularjs';
const ALLOWED = new Set(['.xlsx', '.xls', '.ods', '.csv']);
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 10 * 1024 * 1024 }, // 10 MB
fileFilter: (req, file, cb) => {
cb(null, ALLOWED.has(path.extname(file.originalname).toLowerCase()));
},
});
const app = express();
app.post('/import', upload.single('file'), async (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'Send an .xlsx, .xls, .ods or .csv file in the "file" field' });
}
try {
// No file name needed: the format is detected from the bytes
const result = await tabularjs(req.file.buffer);
res.json({
file: req.file.originalname,
sheets: result.worksheets.map((s) => ({
name: s.worksheetName,
rows: s.data.length,
preview: s.data.slice(0, 3),
})),
});
} catch (err) {
res.status(422).json({ error: err.message });
}
});
app.listen(3000, () => console.log('Listening on http://localhost:3000'));
```
Try it with curl:
```bash
curl -F "file=@orders.xlsx" http://localhost:3000/import
```
```json
{
"file": "orders.xlsx",
"sheets": [
{
"name": "Orders",
"rows": 4,
"preview": [
["Order", "Customer", "Date", "Qty", "Price", "Total", "Paid"],
[1001, "Acme Corp", 45366, 3, 19.99, "=D2*E2", true],
[1002, "Globex", 45369, 8, 5.5, "=D3*E3", false]
]
},
{
"name": "Customers",
"rows": 3,
"preview": [["Name", "Country"], ["Acme Corp", "US"], ["Globex", "DE"]]
}
]
}
```
Why detect from the bytes instead of trusting the name? Users rename files. An `.xls` saved as `report.xlsx` still parses correctly this way, while forcing `extension: 'xlsx'` would fail with a zip error. The file filter on the name is still useful to reject obvious mistakes early.
Two details about uploads:
- The `limits.fileSize` cap matters. TabularJS already guards against zip bombs (each part of an XLSX is capped at 256 MB once inflated), but there is no reason to accept a 500 MB upload in an import form.
- Content that is not a recognized binary format is read as text. A random text file renamed to `.xlsx` returns a one-sheet result instead of an error. Validate what you got, for example by checking that the header row has the columns you expect, before importing it.
The same pattern works in other frameworks: Fastify (`@fastify/multipart` gives you `await part.toBuffer()`), Next.js route handlers (`Buffer.from(await file.arrayBuffer())`, see the [Next.js example](/examples/nextjs-api-route)) and serverless functions. A shorter version is in the [Express upload example](/examples/express-upload).
## Batch-convert a folder
To process every spreadsheet in a directory, list the files, parse each one, and write the result next to it. Catch errors per file so one broken workbook does not stop the run.
```js
import { readdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import tabularjs from 'tabularjs';
const SPREADSHEET = /\.(xlsx|xls|ods|csv)$/i;
const dir = process.argv[2] ?? './data';
const files = (await readdir(dir, { recursive: true }))
.filter((name) => SPREADSHEET.test(name));
const summary = [];
for (const name of files) {
const fullPath = path.join(dir, name);
try {
const result = await tabularjs(fullPath);
const sheets = result.worksheets.map((s) => ({
name: s.worksheetName ?? path.parse(name).name,
rows: s.data,
}));
await writeFile(fullPath + '.json', JSON.stringify(sheets, null, 2));
summary.push({ file: name, sheets: sheets.length, ok: true });
} catch (err) {
summary.push({ file: name, ok: false, error: err.message });
}
}
console.table(summary);
```
```
┌─────────┬───────────────┬────────┬──────┐
│ (index) │ file │ sheets │ ok │
├─────────┼───────────────┼────────┼──────┤
│ 0 │ 'euro.csv' │ 1 │ true │
│ 1 │ 'orders.xls' │ 2 │ true │
│ 2 │ 'orders.xlsx' │ 2 │ true │
│ 3 │ 'people.csv' │ 1 │ true │
└─────────┴───────────────┴────────┴──────┘
```
`readdir` with `recursive: true` needs Node.js 20.1 or later. CSV files have a single sheet without a `worksheetName`, which is why the script falls back to the file name.
Files are processed one after the other on purpose. Each workbook is fully loaded in memory while it is parsed, so running hundreds in parallel with `Promise.all` can exhaust memory on large files. If you need more throughput, process a few at a time, or spread the work over `worker_threads`. Another version of this script is in the [batch directory example](/examples/batch-directory).
## Dates are serial numbers
In the output above, `45366` is a date. Excel stores dates as the number of days since 1899-12-30 and uses the cell's number format to display them. TabularJS returns the stored number and exposes the format in `worksheet.cells`:
```js
import tabularjs from 'tabularjs';
// Excel stores dates as days since 1899-12-30 (the 1900 date system)
function excelDateToJS(serial) {
return new Date(Math.round((serial - 25569) * 86400 * 1000));
}
const [orders] = (await tabularjs('./data/orders.xlsx')).worksheets;
console.log(orders.data[1][2]); // 45366
console.log(orders.cells.C2.format); // 'yyyy-mm-dd'
console.log(excelDateToJS(orders.data[1][2]).toISOString());
// 2024-03-15T00:00:00.000Z
console.log(excelDateToJS(orders.data[3][2]).toISOString());
// 2024-03-20T14:30:00.000Z
```
The fraction is the time of day. Treat the result as UTC: Excel dates have no time zone. Workbooks created on old versions of Mac Excel may use the 1904 date system, where serials are 1462 days smaller; if dates come out four years early, that is the cause.
## Handle errors
`tabularjs` rejects the Promise when it cannot read a file. Wrap calls in `try/catch` and translate the messages that matter to your users:
```js
import tabularjs from 'tabularjs';
async function readSpreadsheet(path) {
try {
return await tabularjs(path);
} catch (err) {
if (err.code === 'ENOENT') throw new Error(`File not found: ${path}`);
if (/encrypted/i.test(err.message)) throw new Error(`${path} is password protected`);
if (/Unsupported file type/.test(err.message)) throw new Error(`${path} is not a spreadsheet`);
throw err;
}
}
```
The common messages are `Unsupported file type: pdf` for a format TabularJS does not read, `Workbook is encrypted with a password` for an `.xls` protected with a password to open, and Node's own `ENOENT` error when the path does not exist. The complete list is in the [API reference](/docs/api#errors).
## Common pitfalls
**Expecting computed values.** Formula cells contain the formula (`'=D2*E2'`), never the result. If you need numbers, compute them in JavaScript from the input cells, or ask for a file exported with values only.
**Using `worksheets[0]` blindly.** Workbooks often have a hidden first sheet (a lookup table, a template). Hidden sheets are returned with `worksheetState: 'hidden'` or `'veryHidden'`. Find the sheet by name instead: `result.worksheets.find((s) => s.worksheetName === 'Orders')`.
**Assuming the header is on row 1.** Real-world reports often have a title, a blank row, then the header. Look for the header row (for example, the first row whose first cell is `'Order'`) and slice from there.
**Writing a file.** TabularJS only reads. To produce an `.xlsx` from Node.js, use a writer such as ExcelJS.
**Very large exports.** Each file is read fully into memory. For CSV exports of several gigabytes, a streaming CSV parser such as `csv-parse` is a better fit than any whole-file reader.
## Related
- [API reference](/docs/api): inputs, options and errors
- [Convert Excel to JSON](/docs/guides/convert-excel-to-json): objects from the header row, all sheets, dates
- [Read an Excel file in the browser](/docs/guides/read-excel-file-javascript)
- [Parse CSV in JavaScript](/docs/guides/parse-csv-javascript)
- Formats: [XLSX](/formats/xlsx), [XLS](/formats/xls), [ODS](/formats/ods)
- Examples: [Node.js basic](/examples/nodejs-basic), [Express upload](/examples/express-upload), [batch directory](/examples/batch-directory), [XLS formulas](/examples/xls-formulas)
---
# Examples
### Parse a file in Node.js
Pass a file path to tabularjs and get a structured JSON document with every worksheet, its name, and its rows.
Source: https://tabularjs.com/examples/nodejs-basic/
```js
import tabularjs from 'tabularjs';
const result = await tabularjs('./data/sales.xlsx');
for (const sheet of result.worksheets) {
console.log(sheet.worksheetName, '->', sheet.data.length, 'rows');
}
```
### Browser: parse from a file input
Wire up a file input to TabularJS: the File object is passed straight through, no FileReader required.
Source: https://tabularjs.com/examples/browser-file-input/
```html
```
### Drag & drop spreadsheet upload
A drop zone that accepts files dropped from the desktop and converts them to JSON instantly.
Source: https://tabularjs.com/examples/drag-and-drop/
```html
Drop a spreadsheet here
```
### React: upload and render
Use TabularJS in React to handle file uploads and display parsed data with useState and a file input ref.
Source: https://tabularjs.com/examples/react/
```jsx
import { useRef, useState } from 'react';
import tabularjs from 'tabularjs';
export default function Uploader() {
const inputRef = useRef(null);
const [data, setData] = useState(null);
const onChange = async (e) => {
const file = e.target.files[0];
if (!file) return;
const result = await tabularjs(file);
setData(result.worksheets[0].data);
};
return (
{data && (
{data.map((row, i) => (
{row.map((cell, j) =>
{cell}
)}
))}
)}
);
}
```
### Vue 3 component
Use TabularJS inside a Vue 3 single-file component with reactive state and a file input.
Source: https://tabularjs.com/examples/vue/
```vue
{{ data }}
```
### Next.js App Router API route
Create a POST route that accepts multipart form data and returns the parsed JSON. Pure server code; the client never needs to load the parser.
Source: https://tabularjs.com/examples/nextjs-api-route/
```ts
// app/api/convert/route.ts
import tabularjs from 'tabularjs';
export async function POST(req: Request) {
const form = await req.formData();
const file = form.get('file') as File;
// Raw bytes: the format is detected from the content
const bytes = new Uint8Array(await file.arrayBuffer());
const result = await tabularjs(bytes);
return Response.json(result);
}
```
### Load into Jspreadsheet CE
Upload a file and instantly render it in the open-source Jspreadsheet CE component, with no data transformation needed.
Source: https://tabularjs.com/examples/jspreadsheet-ce/
```html
```
### Load into Jspreadsheet Pro
Jspreadsheet Pro adds advanced features like pivot tables, charts, and formula engine. Use TabularJS to import files into a Pro workbook.
Source: https://tabularjs.com/examples/jspreadsheet-pro/
```html
```
### Use with LemonadeJS
TabularJS pairs perfectly with LemonadeJS (a ~5kB reactive library from the same team) to build a file preview UI.
Source: https://tabularjs.com/examples/lemonadejs/
```html
```
### Express.js upload endpoint
An Express route that uses Multer for uploads and TabularJS for conversion, ready for production.
Source: https://tabularjs.com/examples/express-upload/
```js
import express from 'express';
import multer from 'multer';
import tabularjs from 'tabularjs';
const app = express();
const upload = multer();
app.post('/convert', upload.single('file'), async (req, res) => {
try {
// Multer keeps the upload in memory as a Buffer; the format is detected from the content
const result = await tabularjs(req.file.buffer);
res.json(result);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
app.listen(3000);
```
### Batch convert a directory
A Node.js script that scans a directory, converts each supported file to JSON, and writes the result alongside.
Source: https://tabularjs.com/examples/batch-directory/
```js
import fs from 'node:fs/promises';
import path from 'node:path';
import tabularjs from 'tabularjs';
const SUPPORTED = /\.(xlsx?|ods|csv|tsv|dbf|xml|slk|dif|wk[1-4s])$/i;
async function walk(dir) {
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) await walk(full);
else if (SUPPORTED.test(entry.name)) {
const result = await tabularjs(full);
await fs.writeFile(full + '.json', JSON.stringify(result, null, 2));
console.log('wrote', full + '.json');
}
}
}
walk(process.argv[2] || '.');
```
### Convert a URL response
Pipe a fetch response through TabularJS without saving to disk, ideal for serverless functions and edge workers.
Source: https://tabularjs.com/examples/csv-streaming/
```js
import tabularjs from 'tabularjs';
const response = await fetch('https://example.com/report.xlsx');
const bytes = await response.arrayBuffer();
// No file name needed: the format is detected from the bytes
const result = await tabularjs(bytes);
console.log(result.worksheets[0].data.slice(0, 5));
```
### Keep formulas from legacy .xls files
Excel 97-2003 files store formulas as binary tokens. TabularJS decodes them back to the text Excel shows, including references to other sheets and workbooks, named ranges and array constants. If a formula cannot be decoded exactly, the cell keeps the value Excel saved.
Source: https://tabularjs.com/examples/xls-formulas/
```js
import tabularjs from 'tabularjs';
const result = await tabularjs('./budget-2003.xls');
const [summary] = result.worksheets;
// Formulas come back as text, exactly as Excel shows them
console.log(summary.data[1]);
// ['Total', "=SUM('Jan:Dec'!B2)", '=Rates!B1*B2', '=SUM(Expenses)']
// The same formula, without the "=", in the cell metadata
console.log(summary.cells.C2.formula); // 'Rates!B1*B2'
// Other formulas the decoder understands
// ='[Prices.xls]Sheet1'!A1 reference to another workbook
// =SUM(B2:B4*{1,2,3}) array constants
// =MYFUNCTION(A1) add-in / user-defined functions
```
### Scrape a page's tables
Point TabularJS at an HTML string and get every
on the page as a worksheet, perfect for quick scraping tasks. The first row of each table becomes its column titles.
Source: https://tabularjs.com/examples/html-table-scrape/
```js
import tabularjs from 'tabularjs';
const html = await fetch('https://example.com/prices').then(r => r.text());
const result = await tabularjs(html);
// One worksheet per
: Table1, Table2...
for (const sheet of result.worksheets) {
console.log(sheet.worksheetName, sheet.columns.map(c => c.title), sheet.data);
}
// Or only one of them
const second = await tabularjs(html, { tableIndex: 1 });
```
---
# Supported formats
### Excel 2007+ (XLSX)
Extensions: .xlsx. Features: formulas, styles, merged, comments, data.
OOXML-based spreadsheet format introduced with Microsoft Office 2007. TabularJS parses XLSX files with its own parser in the browser and Node.js: no SheetJS, no xlsx dependency.
```js
import tabularjs from 'tabularjs';
// Browser
const file = document.querySelector('input[type=file]').files[0];
const result = await tabularjs(file);
console.log(result.worksheets[0].data);
// => [ [ 'Name', 'Age' ], [ 'Alice', 30 ], ... ]
```
### Excel 97-2003 (XLS)
Extensions: .xls. Features: formulas, styles, merged, data.
Binary BIFF-based format used by Excel 97 through 2003. TabularJS decodes BIFF records and PTG tokens to reconstruct formulas and data, including references to other sheets and workbooks, named ranges and array formulas. Write-protected (read-only recommended) workbooks open normally.
```js
import tabularjs from 'tabularjs';
const result = await tabularjs('legacy-report.xls');
console.log(result.worksheets[0].data);
```
### OpenDocument (ODS)
Extensions: .ods. Features: formulas, styles, merged, data.
ISO-standardized spreadsheet format used by LibreOffice, OpenOffice, and Google Sheets exports. TabularJS reads the XML content directly.
```js
import tabularjs from 'tabularjs';
const result = await tabularjs('report.ods');
console.log(result.worksheets.map(w => w.worksheetName));
```
### CSV
Extensions: .csv. Features: data.
The universal data exchange format. TabularJS handles quoted fields, escaped quotes, line breaks inside quotes, and CRLF or LF line endings.
```js
import tabularjs from 'tabularjs';
const result = await tabularjs('data.csv');
const rows = result.worksheets[0].data;
```
### TSV
Extensions: .tsv, .tab. Features: data.
Delimited format using tabs. Common for spreadsheet data copied to the clipboard, database exports, and bioinformatics data.
```js
const result = await tabularjs('export.tsv');
```
### Plain Text
Extensions: .txt. Features: data.
Treats .txt files as tab-delimited data by default, ideal for legacy text dumps and exports from older business software. Pass a delimiter for pipe- or semicolon-separated files.
```js
const result = await tabularjs('dump.txt');
```
### XML Spreadsheet 2003
Extensions: .xml. Features: formulas, styles, merged, comments, data.
An XML-based spreadsheet format introduced with Microsoft Office 2003. TabularJS reads sheets, values, formulas (converted to A1 notation), styles, merged cells and plain-text comments.
```js
const result = await tabularjs('legacy.xml');
```
### DIF
Extensions: .dif. Features: data.
A plain-text interchange format from the VisiCalc era, still offered as a save format by Excel and LibreOffice Calc and found in archived data. TabularJS reads the header, the optional label section and the data values.
```js
const result = await tabularjs('archive.dif');
```
### SYLK
Extensions: .slk, .sylk. Features: formulas, styles, data.
Microsoft's line-based spreadsheet format from the Multiplan era, still offered by Excel as "SYLK (Symbolic Link)". TabularJS decodes cell, format and bounds records, including E-parameter formulas.
```js
const result = await tabularjs('data.slk');
```
### dBase (DBF)
Extensions: .dbf. Features: data, metadata.
dBase, FoxPro, and Clipper database tables. TabularJS reads the field definitions and the records, and returns the field types, lengths and header details alongside the rows.
```js
const result = await tabularjs('customers.dbf');
console.log(result.worksheets[0].columns);
// [{ title: 'ID', name: 'ID', width: 100, type: 'numeric' }, ...]
```
### Lotus 1-2-3
Extensions: .wks, .wk1, .wk3, .wk4, .123. Features: data.
Files from Lotus 1-2-3, the dominant spreadsheet before Excel. TabularJS reads cell values from WKS, WK1, WK3, WK4 and .123 files, including the sheet names of multi-sheet workbooks.
Formula cells return the value Lotus saved with them; the formula itself is not decoded.
```js
const result = await tabularjs('vintage.wk1');
```
### HTML Tables
Extensions: .html, .htm. Features: formulas, styles, merged, comments, data.
Convert any HTML
element (from a file, a fetched page, or a string) into structured worksheet JSON, one worksheet per table. Supports data-formula attributes, merged cells (colspan/rowspan), and inline styles.
```js
import tabularjs from 'tabularjs';
const html = `
Name
Score
Alice
90
`;
// A string starting with "<" is parsed as HTML; each
becomes a worksheet
const result = await tabularjs(html);
```