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:
- A browser-based XLSX to CSV converter with drag-and-drop
- A Node.js script for command-line conversion
- 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.tgzThis 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 xlsxUse 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:
- Loads SheetJS from the CDN
- Reads the uploaded file as an ArrayBuffer
- Parses the XLSX file into a workbook object
- 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.
CommonJS (recommended)
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.jsSolution 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:
- Use CommonJS instead (recommended by SheetJS)
- Add
"type": "module"to package.json - Use
.mjsfile 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'; // ErrorSolution: 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:
- Post-process the CSV when importing
- Use
dynamicTypingin the consuming parser (e.g., Papa Parse) - 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:
- Installing SheetJS from the CDN for the latest version
- Reading files with FileReader (browser) or
readFile(Node.js) - Converting with
sheet_to_csvand appropriate options - Handling multiple sheets by iterating
SheetNames - 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.
Related posts
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 .