Blog
January 11, 2026

Convert XLSX to CSV in JavaScript (browser and Node.js)

15 mins read

Convert XLSX to CSV in JavaScript (browser and Node.js)

Excel files are the de facto standard for business data exchange. At some point, you will need to convert XLSX files to CSV for processing, database imports, or integration with systems that do not support Excel formats.

This guide shows you how to convert XLSX to CSV in JavaScript using the SheetJS library, with complete examples for both browser and Node.js environments.

Prerequisites

  • Node.js 18+ (for Node.js examples)
  • Basic JavaScript/TypeScript knowledge
  • A text editor or IDE

What you'll build

By the end of this tutorial, you'll have:

  1. A browser-based XLSX to CSV converter with drag-and-drop
  2. A Node.js script for command-line conversion
  3. A React component for file uploads

Step 1: installing SheetJS

SheetJS (the xlsx package) is the standard library for reading and writing spreadsheet files in JavaScript. There is an important installation detail to know: the npm registry version is outdated.

Recommended installation (from SheetJS CDN):

npm install https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz

This installs version 0.20.3 from the official SheetJS CDN. The npm registry contains an older version (0.18.5) that is missing features and security updates.

Alternative (npm registry - outdated):

npm install xlsx

Use this only if you cannot install from the CDN URL. Be aware this installs version 0.18.5.

Step 2: browser implementation

Converting XLSX to CSV in the browser requires reading the file with FileReader, then using SheetJS to parse and convert.

Basic browser example

<!DOCTYPE html>
<html>
<head>
  <title>XLSX to CSV Converter</title>
  <script src="https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js"></script>
</head>
<body>
  <input type="file" id="fileInput" accept=".xlsx,.xls" />
  <pre id="output"></pre>

  <script>
    document.getElementById('fileInput').addEventListener('change', handleFile);

    function handleFile(event) {
      const file = event.target.files[0];
      if (!file) return;

      const reader = new FileReader();

      reader.onload = function(e) {
        const data = new Uint8Array(e.target.result);
        const workbook = XLSX.read(data, { type: 'array' });

        // Get the first sheet
        const sheetName = workbook.SheetNames[0];
        const worksheet = workbook.Sheets[sheetName];

        // Convert to CSV
        const csv = XLSX.utils.sheet_to_csv(worksheet);

        // Display the result
        document.getElementById('output').textContent = csv;
      };

      reader.readAsArrayBuffer(file);
    }
  </script>
</body>
</html>

This example:

  1. Loads SheetJS from the CDN
  2. Reads the uploaded file as an ArrayBuffer
  3. Parses the XLSX file into a workbook object
  4. Converts the first sheet to CSV format

React component

Here is a TypeScript React component for XLSX to CSV conversion:

import { useState, useCallback, ChangeEvent, DragEvent } from 'react';
import * as XLSX from 'xlsx';

interface ConversionResult {
  csv: string;
  fileName: string;
  sheetName: string;
}

export function XLSXToCSVConverter() {
  const [result, setResult] = useState<ConversionResult | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [isDragging, setIsDragging] = useState(false);

  const convertFile = useCallback((file: File) => {
    setError(null);
    setResult(null);

    // Validate file type
    const validTypes = [
      'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
      'application/vnd.ms-excel'
    ];

    if (!validTypes.includes(file.type) && !file.name.match(/\.xlsx?$/i)) {
      setError('Please upload an Excel file (.xlsx or .xls)');
      return;
    }

    const reader = new FileReader();

    reader.onload = (e) => {
      try {
        const data = new Uint8Array(e.target?.result as ArrayBuffer);
        const workbook = XLSX.read(data, { type: 'array' });

        if (workbook.SheetNames.length === 0) {
          setError('The Excel file contains no sheets');
          return;
        }

        const sheetName = workbook.SheetNames[0];
        const worksheet = workbook.Sheets[sheetName];
        const csv = XLSX.utils.sheet_to_csv(worksheet);

        setResult({
          csv,
          fileName: file.name.replace(/\.xlsx?$/i, '.csv'),
          sheetName,
        });
      } catch (err) {
        setError('Failed to parse Excel file. The file may be corrupted.');
      }
    };

    reader.onerror = () => {
      setError('Failed to read the file');
    };

    reader.readAsArrayBuffer(file);
  }, []);

  const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      convertFile(file);
    }
  };

  const handleDragOver = (e: DragEvent) => {
    e.preventDefault();
    setIsDragging(true);
  };

  const handleDragLeave = (e: DragEvent) => {
    e.preventDefault();
    setIsDragging(false);
  };

  const handleDrop = (e: DragEvent) => {
    e.preventDefault();
    setIsDragging(false);

    const file = e.dataTransfer.files[0];
    if (file) {
      convertFile(file);
    }
  };

  const downloadCSV = () => {
    if (!result) return;

    const blob = new Blob([result.csv], { type: 'text/csv;charset=utf-8;' });
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = result.fileName;
    link.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div>
      <div
        onDragOver={handleDragOver}
        onDragLeave={handleDragLeave}
        onDrop={handleDrop}
        style={{
          border: `2px dashed ${isDragging ? '#007bff' : '#ccc'}`,
          borderRadius: '8px',
          padding: '40px',
          textAlign: 'center',
          cursor: 'pointer',
          backgroundColor: isDragging ? '#f0f7ff' : 'transparent',
        }}
      >
        <input
          type="file"
          accept=".xlsx,.xls"
          onChange={handleFileChange}
          style={{ display: 'none' }}
          id="xlsx-input"
        />
        <label htmlFor="xlsx-input" style={{ cursor: 'pointer' }}>
          Drop an Excel file here, or click to select
        </label>
      </div>

      {error && (
        <div style={{ color: 'red', marginTop: '16px' }}>
          {error}
        </div>
      )}

      {result && (
        <div style={{ marginTop: '16px' }}>
          <p>
            Converted <strong>{result.sheetName}</strong> from Excel
          </p>
          <button onClick={downloadCSV}>
            Download {result.fileName}
          </button>
          <pre style={{
            marginTop: '16px',
            padding: '16px',
            backgroundColor: '#f5f5f5',
            overflow: 'auto',
            maxHeight: '300px',
          }}>
            {result.csv}
          </pre>
        </div>
      )}
    </div>
  );
}

Step 3: Node.js implementation

For server-side conversion, SheetJS provides file system methods.

SheetJS recommends using CommonJS in Node.js for automatic file system and stream support:

const XLSX = require('xlsx');
const path = require('path');

function convertXLSXToCSV(inputPath, outputPath) {
  // Read the XLSX file
  const workbook = XLSX.readFile(inputPath);

  // Get the first sheet name
  const sheetName = workbook.SheetNames[0];

  // Write as CSV
  XLSX.writeFile(workbook, outputPath, {
    bookType: 'csv',
    sheet: sheetName,
  });

  console.log(`Converted ${inputPath} to ${outputPath}`);
}

// Usage
convertXLSXToCSV('input.xlsx', 'output.csv');

ESM (ECMAScript Modules)

If you must use ESM, you need to manually inject the file system dependency:

import * as XLSX from 'xlsx';
import * as fs from 'fs';

// Inject fs for file operations
XLSX.set_fs(fs);

function convertXLSXToCSV(inputPath, outputPath) {
  const workbook = XLSX.readFile(inputPath);
  const sheetName = workbook.SheetNames[0];

  XLSX.writeFile(workbook, outputPath, {
    bookType: 'csv',
    sheet: sheetName,
  });
}

convertXLSXToCSV('input.xlsx', 'output.csv');

TypeScript Node.js example

import * as XLSX from 'xlsx';
import * as fs from 'fs';
import * as path from 'path';

// Required for ESM in Node.js
XLSX.set_fs(fs);

interface ConversionOptions {
  sheetIndex?: number;
  sheetName?: string;
  delimiter?: string;
  includeBlankRows?: boolean;
}

function convertXLSXToCSV(
  inputPath: string,
  outputPath: string,
  options: ConversionOptions = {}
): void {
  const {
    sheetIndex = 0,
    sheetName,
    delimiter = ',',
    includeBlankRows = true,
  } = options;

  // Verify input file exists
  if (!fs.existsSync(inputPath)) {
    throw new Error(`Input file not found: ${inputPath}`);
  }

  // Read workbook
  const workbook = XLSX.readFile(inputPath);

  // Determine which sheet to convert
  let targetSheet: string;

  if (sheetName) {
    if (!workbook.SheetNames.includes(sheetName)) {
      throw new Error(`Sheet "${sheetName}" not found in workbook`);
    }
    targetSheet = sheetName;
  } else {
    if (sheetIndex >= workbook.SheetNames.length) {
      throw new Error(`Sheet index ${sheetIndex} out of range`);
    }
    targetSheet = workbook.SheetNames[sheetIndex];
  }

  const worksheet = workbook.Sheets[targetSheet];

  // Convert to CSV with options
  const csv = XLSX.utils.sheet_to_csv(worksheet, {
    FS: delimiter,
    blankrows: includeBlankRows,
  });

  // Write output
  fs.writeFileSync(outputPath, csv, 'utf-8');

  console.log(`Converted sheet "${targetSheet}" to ${outputPath}`);
}

// Example usage
convertXLSXToCSV('data.xlsx', 'data.csv', {
  delimiter: ',',
  includeBlankRows: false,
});

Step 4: configuration options

The sheet_to_csv function accepts several options to customize the output:

const csv = XLSX.utils.sheet_to_csv(worksheet, {
  FS: ',',           // Field separator (default: comma)
  RS: '\n',          // Record separator (default: newline)
  dateNF: 'YYYY-MM-DD', // Date format string
  strip: false,      // Remove trailing field separators
  blankrows: true,   // Include blank rows
  skipHidden: false, // Skip hidden rows and columns
  forceQuotes: false, // Force quotes around all fields
});

Common configuration scenarios

Tab-separated values (TSV):

const tsv = XLSX.utils.sheet_to_csv(worksheet, {
  FS: '\t',
});

European Excel format (semicolon delimiter):

Many European Excel installations use semicolons as delimiters. To convert for these systems:

const csv = XLSX.utils.sheet_to_csv(worksheet, {
  FS: ';',
});

Skip hidden rows and columns:

const csv = XLSX.utils.sheet_to_csv(worksheet, {
  skipHidden: true,
});

Note: To preserve hidden row/column information when reading, use the cellStyles option:

const workbook = XLSX.read(data, {
  type: 'array',
  cellStyles: true,
});

Step 5: handling multiple sheets

XLSX files often contain multiple worksheets. Here is how to handle them.

List all sheets

const workbook = XLSX.readFile('multi-sheet.xlsx');

console.log('Available sheets:');
workbook.SheetNames.forEach((name, index) => {
  console.log(`  ${index}: ${name}`);
});

Convert a specific sheet by name

function convertSheetByName(
  workbook: XLSX.WorkBook,
  sheetName: string
): string {
  const worksheet = workbook.Sheets[sheetName];

  if (!worksheet) {
    throw new Error(`Sheet "${sheetName}" not found`);
  }

  return XLSX.utils.sheet_to_csv(worksheet);
}

const csv = convertSheetByName(workbook, 'Sales Data');

Export all sheets to separate CSV files

import * as XLSX from 'xlsx';
import * as fs from 'fs';
import * as path from 'path';

XLSX.set_fs(fs);

function exportAllSheets(inputPath: string, outputDir: string): void {
  const workbook = XLSX.readFile(inputPath);
  const baseName = path.basename(inputPath, path.extname(inputPath));

  // Create output directory if it doesn't exist
  if (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir, { recursive: true });
  }

  workbook.SheetNames.forEach((sheetName, index) => {
    const worksheet = workbook.Sheets[sheetName];
    const csv = XLSX.utils.sheet_to_csv(worksheet);

    // Sanitize sheet name for filename
    const safeSheetName = sheetName.replace(/[^a-z0-9]/gi, '_');
    const outputPath = path.join(outputDir, `${baseName}_${safeSheetName}.csv`);

    fs.writeFileSync(outputPath, csv, 'utf-8');
    console.log(`Exported: ${outputPath}`);
  });
}

exportAllSheets('report.xlsx', './csv-output');

Let users select a sheet (React)

import { useState, ChangeEvent } from 'react';
import * as XLSX from 'xlsx';

export function MultiSheetConverter() {
  const [workbook, setWorkbook] = useState<XLSX.WorkBook | null>(null);
  const [selectedSheet, setSelectedSheet] = useState<string>('');
  const [csv, setCsv] = useState<string>('');

  const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = (event) => {
      const data = new Uint8Array(event.target?.result as ArrayBuffer);
      const wb = XLSX.read(data, { type: 'array' });
      setWorkbook(wb);
      setSelectedSheet(wb.SheetNames[0]);
      setCsv('');
    };
    reader.readAsArrayBuffer(file);
  };

  const handleSheetChange = (e: ChangeEvent<HTMLSelectElement>) => {
    setSelectedSheet(e.target.value);
  };

  const convertSheet = () => {
    if (!workbook || !selectedSheet) return;

    const worksheet = workbook.Sheets[selectedSheet];
    const csvOutput = XLSX.utils.sheet_to_csv(worksheet);
    setCsv(csvOutput);
  };

  return (
    <div>
      <input
        type="file"
        accept=".xlsx,.xls"
        onChange={handleFileUpload}
      />

      {workbook && (
        <div style={{ marginTop: '16px' }}>
          <label>
            Select sheet:
            <select value={selectedSheet} onChange={handleSheetChange}>
              {workbook.SheetNames.map((name) => (
                <option key={name} value={name}>
                  {name}
                </option>
              ))}
            </select>
          </label>

          <button onClick={convertSheet} style={{ marginLeft: '8px' }}>
            Convert to CSV
          </button>
        </div>
      )}

      {csv && (
        <pre style={{
          marginTop: '16px',
          padding: '16px',
          backgroundColor: '#f5f5f5',
          overflow: 'auto',
        }}>
          {csv}
        </pre>
      )}
    </div>
  );
}

Step 6: error handling

Production code needs to handle various error conditions.

import * as XLSX from 'xlsx';

interface ConversionError {
  type: 'file_not_found' | 'invalid_format' | 'empty_file' | 'parse_error';
  message: string;
}

type ConversionResult =
  | { success: true; csv: string; sheetName: string }
  | { success: false; error: ConversionError };

function safeConvertXLSXToCSV(data: ArrayBuffer): ConversionResult {
  try {
    // Check for empty data
    if (!data || data.byteLength === 0) {
      return {
        success: false,
        error: {
          type: 'empty_file',
          message: 'The uploaded file is empty',
        },
      };
    }

    // Attempt to parse
    let workbook: XLSX.WorkBook;
    try {
      workbook = XLSX.read(new Uint8Array(data), { type: 'array' });
    } catch (parseError) {
      return {
        success: false,
        error: {
          type: 'invalid_format',
          message: 'The file is not a valid Excel file',
        },
      };
    }

    // Check for sheets
    if (workbook.SheetNames.length === 0) {
      return {
        success: false,
        error: {
          type: 'empty_file',
          message: 'The Excel file contains no worksheets',
        },
      };
    }

    const sheetName = workbook.SheetNames[0];
    const worksheet = workbook.Sheets[sheetName];

    // Check if sheet has data
    const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1');
    if (range.e.r === 0 && range.e.c === 0 && !worksheet['A1']) {
      return {
        success: false,
        error: {
          type: 'empty_file',
          message: `Sheet "${sheetName}" is empty`,
        },
      };
    }

    const csv = XLSX.utils.sheet_to_csv(worksheet);

    return {
      success: true,
      csv,
      sheetName,
    };
  } catch (error) {
    return {
      success: false,
      error: {
        type: 'parse_error',
        message: error instanceof Error ? error.message : 'Unknown error',
      },
    };
  }
}

// Usage
const result = safeConvertXLSXToCSV(arrayBuffer);

if (result.success) {
  console.log(`Converted ${result.sheetName}:`, result.csv);
} else {
  console.error(`Conversion failed: ${result.error.message}`);
}

Complete example

Here is a complete Node.js CLI tool that converts XLSX files to CSV:

#!/usr/bin/env node
import * as XLSX from 'xlsx';
import * as fs from 'fs';
import * as path from 'path';

// Initialize file system for ESM
XLSX.set_fs(fs);

interface CLIOptions {
  input: string;
  output?: string;
  sheet?: string;
  delimiter?: string;
  allSheets?: boolean;
}

function parseArgs(): CLIOptions {
  const args = process.argv.slice(2);
  const options: CLIOptions = { input: '' };

  for (let i = 0; i < args.length; i++) {
    switch (args[i]) {
      case '-o':
      case '--output':
        options.output = args[++i];
        break;
      case '-s':
      case '--sheet':
        options.sheet = args[++i];
        break;
      case '-d':
      case '--delimiter':
        options.delimiter = args[++i];
        break;
      case '-a':
      case '--all-sheets':
        options.allSheets = true;
        break;
      default:
        if (!args[i].startsWith('-')) {
          options.input = args[i];
        }
    }
  }

  return options;
}

function printUsage(): void {
  console.log(`
Usage: xlsx-to-csv <input.xlsx> [options]

Options:
  -o, --output <file>     Output file path (default: input name with .csv)
  -s, --sheet <name>      Sheet name to convert (default: first sheet)
  -d, --delimiter <char>  Field delimiter (default: comma)
  -a, --all-sheets        Export all sheets to separate files

Examples:
  xlsx-to-csv data.xlsx
  xlsx-to-csv data.xlsx -o output.csv
  xlsx-to-csv data.xlsx -s "Sheet2" -d ";"
  xlsx-to-csv data.xlsx --all-sheets
`);
}

function main(): void {
  const options = parseArgs();

  if (!options.input) {
    printUsage();
    process.exit(1);
  }

  if (!fs.existsSync(options.input)) {
    console.error(`Error: File not found: ${options.input}`);
    process.exit(1);
  }

  const workbook = XLSX.readFile(options.input);
  const baseName = path.basename(options.input, path.extname(options.input));
  const outputDir = path.dirname(options.input);

  if (options.allSheets) {
    // Export all sheets
    workbook.SheetNames.forEach((sheetName) => {
      const worksheet = workbook.Sheets[sheetName];
      const csv = XLSX.utils.sheet_to_csv(worksheet, {
        FS: options.delimiter || ',',
      });

      const safeSheetName = sheetName.replace(/[^a-z0-9]/gi, '_');
      const outputPath = path.join(outputDir, `${baseName}_${safeSheetName}.csv`);

      fs.writeFileSync(outputPath, csv, 'utf-8');
      console.log(`Exported: ${outputPath}`);
    });
  } else {
    // Export single sheet
    const sheetName = options.sheet || workbook.SheetNames[0];

    if (!workbook.SheetNames.includes(sheetName)) {
      console.error(`Error: Sheet "${sheetName}" not found`);
      console.log(`Available sheets: ${workbook.SheetNames.join(', ')}`);
      process.exit(1);
    }

    const worksheet = workbook.Sheets[sheetName];
    const csv = XLSX.utils.sheet_to_csv(worksheet, {
      FS: options.delimiter || ',',
    });

    const outputPath = options.output || `${baseName}.csv`;
    fs.writeFileSync(outputPath, csv, 'utf-8');
    console.log(`Converted "${sheetName}" to ${outputPath}`);
  }
}

main();

Common pitfalls

Large files cause memory errors

For files larger than ~50MB, loading the entire workbook can exhaust available memory:

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory

Solution 1: Increase Node.js memory limit:

node --max-old-space-size=4096 convert.js

Solution 2: For very large files, consider using a streaming library like exceljs:

import Excel from 'exceljs';
import * as fs from 'fs';

async function streamLargeXLSX(inputPath: string, outputPath: string) {
  const workbookReader = new Excel.stream.xlsx.WorkbookReader(inputPath, {});
  const writeStream = fs.createWriteStream(outputPath);

  for await (const worksheetReader of workbookReader) {
    for await (const row of worksheetReader) {
      // Convert row values to CSV format
      const csvRow = row.values
        .slice(1) // Remove empty first element
        .map((val) => {
          if (val === null || val === undefined) return '';
          const str = String(val);
          // Escape quotes and wrap if contains comma or newline
          if (str.includes(',') || str.includes('\n') || str.includes('"')) {
            return `"${str.replace(/"/g, '""')}"`;
          }
          return str;
        })
        .join(',');

      writeStream.write(csvRow + '\n');
    }
    break; // Process first sheet only
  }

  writeStream.end();
}

Dates display incorrectly

Excel stores dates as serial numbers. By default, SheetJS converts them to JavaScript dates, but formatting can vary:

// Read with explicit date handling
const workbook = XLSX.read(data, {
  type: 'array',
  cellDates: true, // Parse dates as Date objects
});

// Format dates in CSV output
const csv = XLSX.utils.sheet_to_csv(worksheet, {
  dateNF: 'YYYY-MM-DD', // ISO format
});

ESM imports fail in Node.js

Error: Cannot use import statement outside a module

Solutions:

  1. Use CommonJS instead (recommended by SheetJS)
  2. Add "type": "module" to package.json
  3. Use .mjs file extension

If using ESM, remember to inject dependencies:

import * as XLSX from 'xlsx';
import * as fs from 'fs';
import { Readable } from 'stream';

XLSX.set_fs(fs);
XLSX.stream.set_readable(Readable);

NextJS: Cannot import fs

In Next.js, you cannot import fs at the top level of pages:

// This fails in Next.js pages
import * as fs from 'fs'; // Error

Solution: Use dynamic import in server-side functions:

import { readFile, set_fs } from 'xlsx';

export async function getServerSideProps() {
  set_fs(await import('fs'));
  const workbook = readFile('./data.xlsx');
  // ...
}

Numbers are converted to text

CSV is a text format. All data becomes strings. If you need to preserve numeric types, consider:

  1. Post-process the CSV when importing
  2. Use dynamicTyping in the consuming parser (e.g., Papa Parse)
  3. Keep data in JSON format instead of CSV

A simpler alternative: ImportCSV

Building a production-ready XLSX converter requires handling:

  • Multiple file formats (XLSX, XLS, CSV)
  • Large file streaming
  • Date and number formatting
  • Multi-sheet selection UI
  • Column mapping when headers do not match your schema
  • Validation before import

ImportCSV is an open-source React component that handles these concerns:

import { CSVImporter } from '@importcsv/react';

<CSVImporter
  onComplete={(data) => {
    // data.rows contains validated, typed data
    console.log(data);
  }}
  columns={[
    { label: 'Name', key: 'name', required: true },
    { label: 'Email', key: 'email', required: true },
    { label: 'Amount', key: 'amount', dataType: 'number' },
  ]}
/>

ImportCSV automatically:

  • Detects file format (CSV, XLSX, XLS)
  • Provides a sheet selector UI for multi-sheet files
  • Handles column mapping with a visual interface
  • Validates data against your schema before import
  • Works in the browser without server-side processing

Summary

Converting XLSX to CSV in JavaScript involves:

  1. Installing SheetJS from the CDN for the latest version
  2. Reading files with FileReader (browser) or readFile (Node.js)
  3. Converting with sheet_to_csv and appropriate options
  4. Handling multiple sheets by iterating SheetNames
  5. Error handling for empty files, invalid formats, and memory limits

For simple one-off conversions, the code patterns in this guide work well. For production applications where users upload files, consider the complexity of format detection, validation, and column mapping when deciding whether to build from scratch or use a purpose-built component.

Wrap-up

CSV imports shouldn't slow you down. ImportCSV aims to expand into your workflow — whether you're building data import flows, handling customer uploads, or processing large datasets.

If that sounds like the kind of tooling you want to use, try ImportCSV .