How to read an Excel file in 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, 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 instead.
Install
Section titled “Install”With a bundler (Vite, webpack, Next.js…):
npm install tabularjsimport tabularjs from 'tabularjs';Without a build step, load the script from a CDN. It defines a global tabularjs function:
<script src="https://cdn.jsdelivr.net/npm/tabularjs/dist/index.min.js"></script>Both give you the same function: tabularjs(file, options), which returns a Promise.
Read a file from an <input type="file">
Section titled “Read a file from an <input type="file">”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.
<input type="file" id="file" accept=".xlsx,.xls" /><pre id="output"></pre>
<script src="https://cdn.jsdelivr.net/npm/tabularjs/dist/index.min.js"></script><script> document.getElementById('file').addEventListener('change', async (e) => { const file = e.target.files[0]; if (!file) return;
try { const result = await tabularjs(file); const sheet = result.worksheets[0];
document.getElementById('output').textContent = `${sheet.worksheetName}: ${sheet.data.length} rows\n` + JSON.stringify(sheet.data.slice(0, 5), null, 2); } catch (err) { document.getElementById('output').textContent = 'Could not read the file: ' + err.message; } });</script>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
Section titled “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:
{ 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.datais 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 below. worksheet.worksheetNameis the tab name. There is nonameproperty.
The full list of properties (styles, merged cells, comments, column widths) is in Output structure.
Show the data in an HTML table
Section titled “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 <script> stays text.
function renderTable(rows, container) { const table = document.createElement('table');
rows.forEach((row, r) => { const tr = table.insertRow(); row.forEach((value) => { const cell = document.createElement(r === 0 ? 'th' : 'td'); cell.textContent = value; tr.appendChild(cell); }); });
container.replaceChildren(table);}
const result = await tabularjs(file);renderTable(result.worksheets[0].data, document.getElementById('preview'));For large sheets, render the first few hundred rows as a preview, or pass the whole result to a grid component. The result can be given directly to Jspreadsheet (jspreadsheet(element, result)), which keeps styles, merged cells and formulas.
Let the user pick a sheet
Section titled “Let the user pick a sheet”worksheets[0] is not always the sheet the user cares about, and it can even be a hidden sheet. Hidden sheets are included in the result with worksheetState set to 'hidden' or 'veryHidden'. A small select fixes both problems:
const result = await tabularjs(file);const visible = result.worksheets.filter((s) => !s.worksheetState);
const select = document.getElementById('sheet');select.replaceChildren(...visible.map((s, i) => new Option(s.worksheetName, i)));select.onchange = () => renderTable(visible[select.value].data, preview);
renderTable(visible[0].data, preview);Read a file with drag and drop
Section titled “Read a file with drag and drop”A drop event carries the files in e.dataTransfer.files. Each entry is a File, so the TabularJS call is the same as with the input. The one thing you must not forget is preventDefault() on dragover; without it the browser opens the file in the tab instead of firing drop.
<div id="drop" style="border: 2px dashed #999; padding: 40px; text-align: center"> Drop an Excel file here</div>
<script> const zone = document.getElementById('drop');
zone.addEventListener('dragover', (e) => { e.preventDefault(); zone.style.background = '#eef'; });
zone.addEventListener('dragleave', () => { zone.style.background = ''; });
zone.addEventListener('drop', async (e) => { e.preventDefault(); zone.style.background = '';
const file = e.dataTransfer.files[0]; if (!file) return;
try { const result = await tabularjs(file); console.log(result.worksheets.map((s) => s.worksheetName)); } catch (err) { alert('Could not read ' + file.name + ': ' + err.message); } });</script>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.
Read an Excel file from a URL with fetch
Section titled “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.
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.
Dates come back as numbers
Section titled “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:
const sheet = result.worksheets[0];
sheet.data[1][2]; // 45366sheet.cells.C2.format; // 'yyyy-mm-dd'To turn the serial number into a JavaScript Date:
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 shows how to convert every date column automatically using the cell formats.
Formulas are not calculated
Section titled “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.
Common pitfalls
Section titled “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.
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.
When something else fits better
Section titled “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
Section titled “Related”- API reference: inputs, options and errors
- Excel 2007+ (.xlsx) and Excel 97-2003 (.xls) format details
- Convert Excel to JSON: rows to objects, all sheets, dates
- JavaScript, React, Vue and Angular integration guides
- Examples: browser file input, drag and drop

