ScreenshotNeo

BlogHow-to

How to Preview Excel Files in JavaScript

Preview XLSX files in the browser with SheetJS: local uploads, remote URLs, dates, large sheets, errors, and practical rendering limits.

By the ScreenshotNeo team1 October 20268 min read

Use the browser File API to obtain the workbook bytes, parse them with SheetJS, select a worksheet, and render it as an HTML table or your own grid. Browsers generally cannot open an arbitrary local path with XLSX.readFile(filename); your page must receive the file through a file input, drag-and-drop, or another permitted browser API.

The smallest working flow is:

const file = input.files[0];
const bytes = await file.arrayBuffer();
const workbook = XLSX.read(bytes);
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
previewContainer.innerHTML = XLSX.utils.sheet_to_html(worksheet);

This produces a useful data preview. It is not a complete reproduction of Excel’s layout, styling, charts, formulas, or interactive behavior. If visual fidelity matters, render into a grid that supports the features your application needs and test it with representative workbooks.

1. Create a minimal browser preview

Install SheetJS in your application, or load the browser build shown in the official examples. The parser API used below is documented in the SheetJS input documentation.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Excel preview</title>
  <script src="https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js"></script>
  <style>
    body { font: 16px system-ui, sans-serif; margin: 2rem; }
    #status { margin: 1rem 0; }
    #preview { overflow: auto; max-height: 70vh; border: 1px solid #ddd; }
    #preview table { border-collapse: collapse; min-width: 100%; }
    #preview td, #preview th { border: 1px solid #ddd; padding: .4rem .6rem; white-space: pre-wrap; }
  </style>
</head>
<body>
  <label>Choose an Excel file
    <input id="file" type="file" accept=".xlsx,.xls,.xlsb,.csv">
  </label>
  <div id="status" role="status"></div>
  <div id="preview"></div>

  <script>
    const input = document.querySelector('#file');
    const status = document.querySelector('#status');
    const preview = document.querySelector('#preview');

    input.addEventListener('change', async () => {
      const file = input.files[0];
      if (!file) return;

      preview.replaceChildren();
      status.textContent = `Reading ${file.name}…`;

      try {
        const bytes = await file.arrayBuffer();
        const workbook = XLSX.read(bytes);
        if (!workbook.SheetNames.length) throw new Error('The workbook has no worksheets.');

        const sheetName = workbook.SheetNames[0];
        const worksheet = workbook.Sheets[sheetName];
        preview.innerHTML = XLSX.utils.sheet_to_html(worksheet);
        status.textContent = `Showing “${sheetName}” from ${file.name}`;
      } catch (error) {
        status.textContent = `Could not preview the file: ${error.message}`;
      }
    });
  </script>
</body>
</html>

2. Add worksheet selection

Most workbooks contain several sheets. Build a selector from workbook.SheetNames and re-render when the user changes it.

function renderSheet(workbook, sheetName, container) {
  const worksheet = workbook.Sheets[sheetName];
  if (!worksheet) throw new Error(`Worksheet not found: ${sheetName}`);
  container.innerHTML = XLSX.utils.sheet_to_html(worksheet);
}

function addSheetPicker(workbook, select, container) {
  select.replaceChildren();
  for (const name of workbook.SheetNames) {
    const option = new Option(name, name);
    select.add(option);
  }
  select.onchange = () => renderSheet(workbook, select.value, container);
  renderSheet(workbook, workbook.SheetNames[0], container);
}

Keep the parsed workbook in memory while the user switches sheets. For very large files, consider parsing only the rows needed for the first view and clearly label any preview limit.

3. Render data with a custom grid

sheet_to_html is convenient, but a custom grid gives you control over virtualization, sorting, filtering, accessibility, and styling. Convert a worksheet to arrays with sheet_to_json:

const rows = XLSX.utils.sheet_to_json(worksheet, {
  header: 1,
  defval: ''
});

function renderGrid(rows, container) {
  const table = document.createElement('table');
  for (const row of rows) {
    const tr = document.createElement('tr');
    for (const value of row) {
      const td = document.createElement('td');
      td.textContent = value == null ? '' : String(value);
      tr.appendChild(td);
    }
    table.appendChild(tr);
  }
  container.replaceChildren(table);
}

Assign text with textContent, not innerHTML, when values come from an uploaded or remote workbook. This prevents cell contents from being interpreted as markup.

4. Preview an Excel file from a URL

Fetch the resource as binary data and pass the resulting ArrayBuffer to XLSX.read. Do not decode arbitrary workbook bytes as UTF-8 text. The official network example follows this pattern: SheetJS import examples.

async function loadWorkbookFromUrl(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`Download failed: ${response.status} ${response.statusText}`);
  }
  const bytes = await response.arrayBuffer();
  return XLSX.read(bytes);
}

loadWorkbookFromUrl('/files/report.xlsx')
  .then(workbook => {
    const worksheet = workbook.Sheets[workbook.SheetNames[0]];
    document.querySelector('#preview').innerHTML = XLSX.utils.sheet_to_html(worksheet);
  })
  .catch(error => {
    document.querySelector('#status').textContent = error.message;
  });

Cross-origin requirements

The file host must allow the browser request with appropriate CORS headers. A valid URL can still fail in browser JavaScript when the server does not grant your page access. If you control the server, configure Access-Control-Allow-Origin for the required origin. Otherwise proxy the download through your own backend, subject to the file owner’s authorization and your security policy.

5. Parsing options that affect previews

SheetJS exposes parser options in its parse options reference. Set options deliberately and document their effect in the UI.

Option Use Preview implication
cellDates: true Convert date serials to JavaScript Date objects. Dates can be formatted as dates directly. Without it, dates are commonly numeric cells with number formats.
sheetRows: n Limit parsed worksheet rows. Bounds work and memory for a quick preview, but the UI must say that rows were truncated.
cellFormula: true Preserve formula expressions when supported. Displaying a formula is different from recalculating it. Do not promise Excel calculation behavior.
cellNF: true Retain number-format strings. Useful when your renderer will format dates, currency, or percentages.
const workbook = XLSX.read(bytes, {
  cellDates: true,
  sheetRows: 5000,
  cellNF: true
});

6. What this preview does and does not reproduce

A generated HTML table is a cell-data view. It may not reproduce Excel’s complete visual layout, conditional formatting, charts, images, macros, pivot behavior, or interactive controls. SheetJS describes its Community Edition around extracting and generating spreadsheet data, while its product overview places richer styling and additional features in Pro: SheetJS Pro overview. Verify the exact feature set you need with real workbooks.

Test at least these cases before choosing a renderer:

  • merged cells and non-rectangular ranges;
  • hidden rows, columns, and worksheets;
  • formulas and cached formula results;
  • dates, times, percentages, and currency formats;
  • rich text, hyperlinks, comments, and images;
  • charts, pivot tables, macros, and protected sheets;
  • the largest file your users are expected to upload.

7. Drag-and-drop input

const dropZone = document.querySelector('#drop-zone');

dropZone.addEventListener('dragover', event => {
  event.preventDefault();
  dropZone.classList.add('dragging');
});

dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragging'));

dropZone.addEventListener('drop', async event => {
  event.preventDefault();
  dropZone.classList.remove('dragging');
  const file = event.dataTransfer.files[0];
  if (!file) return;

  try {
    const workbook = XLSX.read(await file.arrayBuffer());
    const worksheet = workbook.Sheets[workbook.SheetNames[0]];
    document.querySelector('#preview').innerHTML = XLSX.utils.sheet_to_html(worksheet);
  } catch (error) {
    document.querySelector('#status').textContent = `Could not parse ${file.name}: ${error.message}`;
  }
});

8. Troubleshooting

Symptom Likely cause Fix
readFile is not a function or a filesystem error Browser code is trying to open a path. Use input.files[0].arrayBuffer(), drag-and-drop, or a fetched response, then call XLSX.read.
Remote request fails before parsing Non-2xx response, CORS, authentication, or a redirect policy. Check response.ok, inspect the network response, and configure CORS or use an authorized server-side proxy.
“Unsupported file” or parse exception Truncated download, wrong content, encrypted workbook, or an unsupported feature. Check the status and content length, confirm the downloaded bytes are the workbook, and test the file in a desktop spreadsheet application.
Dates show as numbers Date serials and number formats are being rendered as raw values. Try cellDates: true and apply an explicit date formatter in your grid.
Browser freezes on a large workbook Parsing and HTML generation run on the main thread. Set a preview row limit, render incrementally, use a virtualized grid, or move parsing to a Web Worker.
Cells contain unexpected markup Workbook values were inserted with innerHTML. Use textContent for cell values and sanitize any intentionally generated markup.

9. Performance, reliability, and security

  • Bound the preview: sheetRows can keep initial work predictable. Tell users when the display is partial.
  • Avoid one huge DOM: HTML for every cell is expensive. Use pagination or row virtualization for large sheets.
  • Keep parsing off the UI thread: a Web Worker prevents scrolling and controls from freezing during parsing.
  • Validate uploads: enforce size limits, accept only formats you support, and treat workbook content as untrusted input.
  • Do not execute workbook code: a preview should not run macros or arbitrary embedded content.
  • Handle cancellation: use an AbortController for remote downloads and ignore stale results when users select another file.
  • Measure representative files: parsing time and memory depend on workbook structure, dimensions, and features; do not assume one file’s behavior represents all uploads.

10. Or skip the browser setup

If your goal is to capture a rendered workbook preview or documentation page as an image or PDF, ScreenshotNeo provides a GET endpoint for a URL. It does not parse an XLSX file; your application still needs to convert the workbook into a page first. Then capture that page with one request.

See the ScreenshotNeo API documentation for parameters and response details.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.com/excel-preview \
  -o preview.webp

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com/excel-preview"},
    timeout=90,
)
r.raise_for_status()
open("preview.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com/excel-preview'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('preview.webp', data));

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers identify the page verdict and whether the shot was billed. ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf tools for AI agents. The free plan includes 1,000 screenshots per month with no card, and paid plans start at $5 for 3,000 shots.

Start with 1,000 free screenshots a month.

11. FAQ

Can JavaScript preview an XLSX file without uploading it?

Yes. A browser can parse a file selected with an input or dropped onto the page locally. The bytes can remain in the browser unless your application sends them elsewhere.

Should I use an HTML table or a spreadsheet grid?

Use an HTML table for a simple data inspection view. Choose a grid when you need virtualization, sorting, editing, frozen panes, or richer interaction.

Can SheetJS make the preview look exactly like Excel?

Not from the basic parse-to-HTML flow. Confirm required formatting and feature support with representative files and choose a renderer that explicitly supports those requirements.

Why are dates inconsistent between files?

Excel stores dates as serial values with number formats. Parsing options and your renderer determine whether users see a date, a number, or a formatted string.

Can I preview a workbook hosted on another domain?

Only when the host permits the browser request through CORS or your backend fetches it on the user’s behalf.