How I Convert Excel to PDF in the Browser with Vue 3, xlsx, and html2pdf.js

sunshey
2026-06-16T12:45:31Z
I needed a dead-simple way to turn spreadsheets into PDFs without sending them to a server. Most tools want you to upload the file, wait for processing, then download it. For invoices, budgets, and payroll sheets that's a dealbreaker.
So I built en.sotool.top/excel-to-pdf/ — pick a spreadsheet, preview it, get a PDF. All in the browser. No server involved.
Here's how it works under the hood with Vue 3, xlsx, and html2pdf.js.
Why Client-Side?
The obvious reason is privacy. Financials, payroll, client data — people don't want them on a stranger's server. Beyond that:
- No upload bandwidth limits
- No file size caps from your backend
- Nothing to clean up on a server
- Works offline after the page loads
The catch? You're limited by what the browser can do. For Excel-to-PDF conversion, xlsx gives you the cell data and html2pdf.js renders it to PDF, which covers most real-world spreadsheets.
The Stack
- Vue 3 — UI and state
- xlsx — Parse .xlsx, .xls, .csv, and .ods files
- html2pdf.js — Render HTML to PDF
- File API — Read the uploaded spreadsheet
npm install xlsx html2pdf.js
Reading the Spreadsheet
First, read the file into a binary string and parse it with xlsx.
import * as XLSX from 'xlsx';
async function parseExcel(file) {
const buffer = await file.arrayBuffer();
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
const workbook = XLSX.read(binary, { type: 'binary' });
return workbook;
}
xlsx handles .xlsx, .xls, .csv, and .ods without extra configuration.
Turning a Worksheet into HTML
xlsx gives you raw cell data. The simplest way to preview and print it is to build an HTML table.
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function worksheetToHtml(worksheet) {
const range = worksheet['!ref'] ? XLSX.utils.decode_range(worksheet['!ref']) : null;
if (!range) return '<p>Empty sheet</p>';
let html = '<table style="width:100%;border-collapse:collapse;font-size:13px;">';
for (let R = range.s.r; R <= range.e.r; ++R) {
html += '<tr style="page-break-inside:avoid;">';
for (let C = range.s.c; C <= range.e.c; ++C) {
const cellRef = XLSX.utils.encode_cell({ r: R, c: C });
const cell = worksheet[cellRef];
const value = cell ? (cell.w !== undefined ? String(cell.w) : String(cell.v ?? '')) : '';
const tag = R === range.s.r ? 'th' : 'td';
const style = tag === 'th'
? 'background:#f3f4f6;font-weight:600;border:1px solid #d1d5db;padding:6px 10px;'
: 'border:1px solid #d1d5db;padding:6px 10px;';
html += `<${tag} style="${style}">${escapeHtml(value)}</${tag}>`;
}
html += '</tr>';
}
html += '</table>';
return html;
}
I treat the first row as a header for styling. page-break-inside: avoid helps keep rows from being split across PDF pages.
Converting HTML to PDF
Once the table is rendered in the DOM, html2pdf.js takes over.
import html2pdf from 'html2pdf.js';
async function convertElementToPdf(element, filename) {
await html2pdf()
.from(element)
.set({
margin: [10, 10, 10, 10],
filename,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'landscape' },
pagebreak: { mode: ['css', 'legacy'] },
})
.save();
}
Landscape A4 works well for wide spreadsheets. Portrait is fine too if your data is narrow.
Handling Multiple Sheets
For workbooks with multiple sheets, I build a temporary container with each sheet as a section, then convert the whole thing.
async function convertAllSheets(sheets, filename) {
const container = document.createElement('div');
container.style.backgroundColor = '#fff';
sheets.forEach((sheet, index) => {
const sheetDiv = document.createElement('div');
sheetDiv.style.padding = '20px';
sheetDiv.style.minHeight = '297mm';
if (index > 0) sheetDiv.style.pageBreakBefore = 'always';
const title = document.createElement('h2');
title.textContent = sheet.name;
title.style.cssText = 'font-size:16px;font-weight:600;margin-bottom:12px;';
sheetDiv.appendChild(title);
const content = document.createElement('div');
content.innerHTML = sheet.html;
sheetDiv.appendChild(content);
container.appendChild(sheetDiv);
});
document.body.appendChild(container);
await convertElementToPdf(container, filename);
document.body.removeChild(container);
}
This keeps the output as one PDF while making each sheet easy to find.
Vue 3 Integration
The UI is straightforward: file input, preview area, convert button.
<script setup>
import { ref, computed } from 'vue';
import * as XLSX from 'xlsx';
import html2pdf from 'html2pdf.js';
const file = ref(null);
const sheets = ref([]);
const activeSheet = ref('');
const previewRef = ref(null);
const activeHtml = computed(() => {
const sheet = sheets.value.find(s => s.name === activeSheet.value);
return sheet ? sheet.html : '';
});
async function handleUpload(event) {
const uploaded = event.target.files[0];
if (!uploaded) return;
const workbook = await parseExcel(uploaded);
sheets.value = workbook.SheetNames.map(name => ({
name,
html: worksheetToHtml(workbook.Sheets[name])
}));
activeSheet.value = sheets.value[0]?.name || '';
file.value = uploaded;
}
async function downloadPdf() {
if (!previewRef.value || !file.value) return;
const filename = file.value.name.replace(/\.xlsx?$/i, '') + '.pdf';
await convertElementToPdf(previewRef.value, filename);
}
</script>
<template>
<div>
<input type="file" accept=".xlsx,.xls,.csv,.ods" @change="handleUpload" />
<div v-if="activeHtml" ref="previewRef" v-html="activeHtml" />
<button @click="downloadPdf" v-if="activeHtml">Download PDF</button>
</div>
</template>
Lessons Learned
Spreadsheets are messy. Merged cells, hidden sheets, named ranges, and formulas can all cause surprises. The safest approach is to flatten everything into a simple table.
HTML is the bridge. xlsx doesn't render Excel layouts. Building an HTML table and letting html2pdf.js handle pagination is the most reliable browser-side approach.
Charts and graphs are hard. If the spreadsheet contains charts, they won't come through with xlsx alone. For those, a desktop tool is still the better choice.
Landscape helps. Wide tables get cut off in portrait A4. Defaulting to landscape gives you more usable width.
Try It
The converter is live at en.sotool.top/excel-to-pdf/.
Free, no signup, nothing uploads to a server.
Full source is on GitHub. The Excel-to-PDF logic is in src/views/ExcelToPdf.vue.
Want More Advanced PDF Tools?
If you need OCR, form editing, digital signatures, or batch processing, Wondershare PDFelement is a solid desktop option. It keeps everything local.
This post contains affiliate links.
Have you built client-side spreadsheet tools? What library did you use?